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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

.vscode/
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"liveServer.settings.port": 5501,
"githubPullRequests.ignoredPullRequestBranches": [
"main"
]
}
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://recipe-assignment-technigo.netlify.app/
289 changes: 289 additions & 0 deletions backup-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
// ==================================================
// Settings & Constants
// API keys, URLs, and initial setup variables
// ==================================================
document.addEventListener("DOMContentLoaded", () => {

const myHeaders = new Headers();
myHeaders.append('x-api-key', '0c625ab60e6a40a1a4b6cc2e4a5fe9b0');
// Tells the API that we want the data in JSON format
myHeaders.append('Content-Type', 'application/json')

const requestOptions = {
method: 'GET', // We want to GET (fetch) data
redirect: 'follow',
headers: myHeaders // API-key sends as a HTTP-header, more safe without the key in the URL
};
Comment on lines +7 to +16

Choose a reason for hiding this comment

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

great that you have investigated in this. We will learn more about storing API keys in a secret and secure way


// The address of the API we fetch data from
const URL = 'https://api.spoonacular.com/recipes/random?number=30';

// === Connecting JavaScript variables to HTML elements ===

const randomBtn = document.getElementById("random-recipe");
const cardOverlay = document.getElementById("card-overlay");
const cardContent = document.getElementById("card-content");

const glutenBtn = document.getElementById("gluten-btn");
const dairyBtn = document.getElementById("dairy-btn");
const vegBtn = document.getElementById("veg-btn");
const allBtn = document.getElementById("filter-btn");
const descenBtn = document.getElementById("descending-btn");
const ascenBtn = document.getElementById("ascending-btn");

// Get all buttons in the filter section into a single list
const filterButtons = document.querySelectorAll(".filter button")
const sortButtons = document.querySelectorAll(".sort button")

// An empty list that will act as our local "database" for all recipes
let allRecipes = [];

// ==================================================
// Functions
// ==================================================

// A "display engine" that can take any list of recipes and show it on the screen
const displayRecipes = (recipesToShow) => {

// Finds the container for all the recipe cards
const recipeContainer = document.getElementById("recipe-container");

//if (recipesToShow) is flase, undefined - length === 0, checking the list, if it contains any recipes
// || = if anyone of these two things is true - show empty-state-message
if (!recipesToShow || recipesToShow.length === 0) {
recipeContainer.innerHTML = '<p class="empty-state-message">Sorry, no recipes match your filter. Please try another one!</p>';
return; // terminate the function otherwise it would runt map() with an empty list
}
Comment on lines +53 to +56

Choose a reason for hiding this comment

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

good fallback to have an empty state


// Builds all the cards in memory first and then adds them to the page all at once
const allCardsHTML = recipesToShow.map(recipe => createGridCardHTML(recipe)).join('');
recipeContainer.innerHTML = allCardsHTML;

};

// A function for building the HTML code for a small recipe card in the grid
const createGridCardHTML = (recipe) => {
const imgUrl = recipe.image;
const title = recipe.title;
const time = recipe.readyInMinutes;
const diets = recipe.diets;
const ingredients = recipe.extendedIngredients || []; // Use an empty list if ingredients are missing
//Go through every ingredient in the list..
const ingredientsHTML = ingredients.map(ingredient => {
return `<li>${ingredient.original}</li>`; //..and make an HTML-list <li> for each item
}).join(''); //then paste them together in one string

//Building the HTML were we paste the variables that was created above ${...}
return `
<article class="recipe-card">
<img src="${imgUrl}" alt="Picture of ${title}">
<div class="grid-card-content">
<h2 class="recipe-title">${title}</h2>
<p class="recipe-time"> Cooking Time: <span class="time-answer">${time} minutes </span></p>
<p>Diets:<span class="diet-answer"> ${diets}</span></p>
<h3 class="ingredients-title">Ingredients:</h3>
<ul class="recipe-ingredients">
${ingredientsHTML}
</ul>
</div>
</article>
`;
};

// A function for building the HTML for the large, detailed card in the popup window
const createOverlayCardHTML = (recipe) => {
const imgUrl = recipe.image;
const title = recipe.title;
const time = recipe.readyInMinutes;
const diets = recipe.diets;

const ingredients = recipe.extendedIngredients || [];//the list with ingrediens, if its empty make an empty list to avoid errors
const instructions = recipe.instructions;
//Go through every ingredient in the list..
const ingredientsHTML = ingredients.map(ingredient => {
return `<li>${ingredient.original}</li>`; //..and make an HTML-list <li> for each item
}).join(''); //Then paste them together in one string

//Building the HTML were we paste the variables that was created above ${...}
return `
<div class="overlay-card-content">
<img src="${imgUrl}" alt="Picture of ${title}">
<h2 class="recipe-title">${title}</h2>
<p class="recipe-time"> Cooking Time: <span class="time-answer">${time} minutes </span></p>
<p>Diets:<span class="diet-answer"> ${diets}</span></p>
<h3 class="ingredients-title">Ingredients:</h3>
<ul class="recipe-ingredients">
${ingredientsHTML}
</ul>
<p class="recipe-instructions">${instructions}</p>
</div>
`;
};

// A function that runs one time at the start to handle the incoming data
const processRecipeData = (result) => {
// Checks that we actually received a valid list of recipes
if (result && result.recipes && result.recipes.length > 0) {
// Saves all recipes to our local "database"
allRecipes = result.recipes;
console.log("All Recipes:", allRecipes);

// Uses our "display engine" to show all recipes from the start
displayRecipes(allRecipes);

} else {
// If the data was empty or invalid
console.log("Could not process recipe data, result was empty or invalid");
}
};

// Listens for clicks on the "random" button
randomBtn.addEventListener("click", () => {
// First, a safety check to make sure there are recipes in our list
// If the list doesn't exist or is empty...
if (!allRecipes || allRecipes.length === 0) {
// ...log a message to the console...
console.log("No recipes available to choose from.");
// ...and stop the function here
return;
}

// Selects a random index from our allrecipes list
const randomIndex = Math.floor(Math.random() * allRecipes.length);
const randomRecipe = allRecipes[randomIndex];

// Builds the HTML for the popup window with the random recipe
const cardHTML = createOverlayCardHTML(randomRecipe);
cardContent.innerHTML = cardHTML;// Puts the HTML into the white box
cardOverlay.classList.add("visible");// Makes the entire popup window visible
});

// Listens for clicks on the dark background to close the popup
cardOverlay.addEventListener("click", (event) => {
// Checks if the clicked element was the background itself, and not the box inside it
if (event.target === cardOverlay) {
cardOverlay.classList.remove("visible");// Hides the popup window.
Comment on lines +162 to +165

Choose a reason for hiding this comment

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

}
});

//activates the class "active", witch removes and ads the colors for the clicked buttons
const updateActiveButton = (clickedButton) => {
// First, remove the "active" class from all sort buttons
filterButtons.forEach(button => {
button.classList.remove("active");
});
// Then, add the "active" class to the one that was just clicked
clickedButton.classList.add("active");
};

//activates the class "active", witch removes and ads/removes the colors for the clicked buttons
const updateActiveSortButton = (clickedButton) => {
// First, remove the "active" class from all sort buttons
sortButtons.forEach(button => {
button.classList.remove("active");
});
// Then, add the "active" class to the one that was just clicked
clickedButton.classList.add("active");
};

// ==================================================
// Event Listeners for Filtering
// ==================================================

// Listens for clicks on the "Gluten free" button
glutenBtn.addEventListener("click", () => {
updateActiveButton(glutenBtn); // Set this button as active
console.log("Filtering for 'gluten free'");
// Creates a new list that only contains recipes where the 'diets' list includes 'gluten free'
const filtered = allRecipes.filter(recipe => recipe.diets.includes('gluten free'));

// Uses our "display engine" to show the new, filtered list
displayRecipes(filtered);
});

// Listens for clicks on the "Dairy free" button
dairyBtn.addEventListener("click", () => {
updateActiveButton(dairyBtn);
console.log("Filtering for 'dairy free'");
const filtered = allRecipes.filter(recipe => recipe.diets.includes('dairy free'));
displayRecipes(filtered);
});

// Listens for clicks on the "Vegetarian" button
vegBtn.addEventListener("click", () => {
updateActiveButton(vegBtn);
console.log("Filtering for 'vegetarian'");
// Some recipes use 'vegetarian: true' instead of in the 'diets' list, so we check that property
const filtered = allRecipes.filter(recipe => recipe.vegetarian === true);
displayRecipes(filtered);
});

// Listens for clicks on the "All" button to reset the filter
allBtn.addEventListener("click", () => {
updateActiveButton(allBtn);
console.log("Showing all recipes again");
// Calls the "display engine" with the entire, unfiltered allrecipes list
displayRecipes(allRecipes);
});

descenBtn.addEventListener("click", () => {
updateActiveSortButton(descenBtn);
console.log("Sorting recipes by descending cooking time");
// Making a copy of allRecipes list and sorting it
const sortedRecipes = [...allRecipes].sort((a, b) => b.readyInMinutes - a.readyInMinutes);
displayRecipes(sortedRecipes);
});

ascenBtn.addEventListener("click", () => {
updateActiveSortButton(ascenBtn);
console.log("Sorting recipes by ascending cooking time");
// Making a copy of allRecipes list and sorting it
const sortedRecipes = [...allRecipes].sort((a, b) => a.readyInMinutes - b.readyInMinutes);
displayRecipes(sortedRecipes);
});

// ==================================================
// Initialization
// This is the code that runs when the page loads
// ==================================================

// Checks if there are saved recipes in the browser's memory (localStorage)
const savedRecipeJSON = localStorage.getItem("savedRecipe");

// IF there are saved recipes...
if (savedRecipeJSON) {
console.log("Fetching recipes from localStorage...");
const savedRecipe = JSON.parse(savedRecipeJSON); // Converts the text back into an object
processRecipeData(savedRecipe);// Uses the saved
// ELSE (if it's the first visit or the memory has been cleared)...
} else {
console.log("Fetching new recipes from the API..."); // Make a call to the API
fetch(URL, requestOptions)
.then(response => {
// If the response did not go wel ( status 402)
if (!response.ok) {
// If the error is specifically "API limit reached"
if (response.status === 402) {
// Error message
throw new Error('API daily limit reached. Please try again tomorrow.');
}
// Other errors
throw new Error('Could not fetch recipes from the server.');
}
// If everyhting went well
return response.json(); // Converts the response to a JavaScript object
})
.then(result => {
// When we have the data:
// 1. Save it to the browser's memory for next time
localStorage.setItem("savedRecipes", JSON.stringify(result));
// 2. Use the data to build the page
processRecipeData(result);
})
.catch(error => {
// Catch the error created above and show it to the user
console.error("error", error);
document.getElementById("recipe-container").innerHTML = `<p class="error-message">${error.message}</p>`;
}); // Catches any errors during the API call
}
});
71 changes: 71 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!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>

<div class="container">

<!-- ==================================================
Header Section
================================================== -->
<header>
<h1>Recipe Library</h1>

<!-- ==================================================
Controls Section (Filter, Sort, Random)
================================================== -->
<section class="select-section">

<div class="filter">
<h2>Filter on diets</h2>
<button id="filter-btn">All</button>
<button id="gluten-btn">Gluten free</button>
<button id="dairy-btn">Dairy free</button>
<button id="veg-btn">Vegetarian</button>
</div>

<!-- Sorting controls -->
<div class="sort">
<h2>Sort on time</h2>
<button id="descending-btn">Descending</button>
<button id="ascending-btn">Ascending</button>
</div>

<!-- Random recipe control -->
<div class="random-section">
<h2>Random Recipe</h2>
<button id="random-recipe">Random</button>
</div>

</section>
</header>

<!-- ==================================================
Main Content (Recipe Grid)
================================================== -->
<!-- The main area where all recipe cards will be displayed -->
<main id="recipe-container" class="recipe-container"></main>

<!-- ==================================================
Overlay / Modal Section
================================================== -->
<!-- An overlay that covers the entire page. It's invisible by default -->
<div id="card-overlay" class="card-overlay">
<!-- The white box inside the overlay that will contain the detailed recipe card -->
<div id="card-content" class="card-content"></div>
</div>

</div>

<!-- ==================================================
Scripts
================================================== -->
<script src="backup-script.js"></script>

Choose a reason for hiding this comment

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

why is it called backup-script?

Copy link
Author

Choose a reason for hiding this comment

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

Did not want the first one to crash with the changes that I did in my first script.js, but when I finished the backup script I went with that instead :)


</body>
</html>
Loading