diff --git a/RecipeFinder/shivt-F5/README.md b/RecipeFinder/shivt-F5/README.md new file mode 100644 index 000000000..dfd364861 --- /dev/null +++ b/RecipeFinder/shivt-F5/README.md @@ -0,0 +1,41 @@ +# RecipeFinder + +A simple and clean **Recipe Finder** web application that lets users search for meals by ingredients. It also highlights whether recipes are vegetarian without needing to click and open each one. Built as a contribution to the JavaScript Mini Projects collection. + + + +--- + +##Features + +- Search recipes by one or more ingredients +- Displays image cards of matching recipes +- Click any card to view full instructions and ingredient list +- Vegetarian-friendly: automatically flags recipes +- Responsive layout and smooth user experience +- Enter and Escape key shortcuts for quick interaction + +--- + +##Tech Stack + +- *Language*: HTML, CSS, JavaScript +- *API Used*: [TheMealDB](https://www.themealdb.com/) + +--- + +##API Information + +This project uses [TheMealDB](https://www.themealdb.com/) which is a free and public API — **no API key is required**. You can directly query endpoints like: + +- `https://www.themealdb.com/api/json/v1/1/filter.php?i=ingredient` +- `https://www.themealdb.com/api/json/v1/1/lookup.php?i=mealID` + +--- + +##How to Run Locally +- Clone the repository, eg. in bash: + git clone https://github.com/your-username/javascript-mini-projects.git + cd javascript-mini-projects/RecipeFinder/shivt-F5 +- Open the app: + Open RecipeFinder.html in your browser diff --git a/RecipeFinder/shivt-F5/RecipeFinder.html b/RecipeFinder/shivt-F5/RecipeFinder.html new file mode 100644 index 000000000..c5e4998fe --- /dev/null +++ b/RecipeFinder/shivt-F5/RecipeFinder.html @@ -0,0 +1,29 @@ + + + +
+ + +Please enter at least one ingredient.
"; + return; + } + + resultsDiv.innerHTML = "Searching recipes...
"; + + Promise.all( + ingredients.map((ing) => + fetch(`https://www.themealdb.com/api/json/v1/1/filter.php?i=${ing}`) + .then((res) => res.json()) + .then((data) => data.meals || []) + ) + ) + .then((results) => { + if (results.some((arr) => arr.length === 0)) { + resultsDiv.innerHTML = "No recipes found with those ingredients.
"; + return; + } + + const commonMeals = results.reduce((acc, curr) => { + const currIds = curr.map((m) => m.idMeal); + return acc.filter((m) => currIds.includes(m.idMeal)); + }, results[0]); + + if (!commonMeals.length) { + resultsDiv.innerHTML = "No recipes found with those ingredients.
"; + return; + } + + resultsDiv.innerHTML = "Loading recipe details...
"; + + return loadMealDetailsWithVegFlag(commonMeals); + }) + .then((mealsWithVegInfo) => { + if (!mealsWithVegInfo) return; + + resultsDiv.innerHTML = ""; + mealsWithVegInfo.forEach(({ mealDetails, isVegetarian }) => { + const card = document.createElement("div"); + card.className = "recipe"; + card.innerHTML = ` +Error loading recipe details.
"; + }); +} + +function loadMealDetailsWithVegFlag(meals) { + const meatKeywords = [ + "chicken", "beef", "egg", "eggs", "prawns", "pork", "fish", "shrimp", "meat", + "bacon", "ham", "lamb", "turkey", "anchovy", "crab", "duck", "salmon", + "sausage", "veal", "venison", "shellfish", "octopus", "squid" + ]; + + return Promise.all( + meals.map((meal) => + fetch(`https://www.themealdb.com/api/json/v1/1/lookup.php?i=${meal.idMeal}`) + .then((res) => res.json()) + .then((data) => { + const mealDetails = data.meals[0]; + let isVegetarian = true; + + for (let i = 1; i <= 20; i++) { + const ing = mealDetails[`strIngredient${i}`]; + if (ing && meatKeywords.some((keyword) => ing.toLowerCase().includes(keyword))) { + isVegetarian = false; + break; + } + } + + return { mealDetails, isVegetarian }; + }) + ) + ); +} + +function showRecipe(idMeal) { + fetch(`https://www.themealdb.com/api/json/v1/1/lookup.php?i=${idMeal}`) + .then((res) => res.json()) + .then((data) => { + const meal = data.meals[0]; + let ingredients = "${meal.strInstructions}
+ `; + + modal.style.display = "block"; + ingredientInput.focus(); + }) + .catch((err) => { + console.error(err); + modalBody.innerHTML = "Error loading recipe details.
"; + modal.style.display = "block"; + }); +} + +searchBtn.addEventListener("click", doSearch); + +ingredientInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + doSearch(); + } +}); + +closeModal.addEventListener("click", () => { + modal.style.display = "none"; +}); + +window.addEventListener("click", (e) => { + if (e.target === modal) { + modal.style.display = "none"; + } +}); + +window.addEventListener("keydown", (e) => { + if (e.key === "Escape" && modal.style.display === "block") { + modal.style.display = "none"; + } +}); diff --git a/RecipeFinder/shivt-F5/style.css b/RecipeFinder/shivt-F5/style.css new file mode 100644 index 000000000..3c061c308 --- /dev/null +++ b/RecipeFinder/shivt-F5/style.css @@ -0,0 +1,127 @@ +body { + font-family: Arial, sans-serif; + text-align: center; + background: #f8f8f8; + color: #333; +} + +.container { + padding: 2rem; +} + +input, button { + padding: 0.5rem; + margin: 1rem; + font-size: 1rem; + border-radius: 6px; + border: 1.5px solid #1b3a6b; /* darker blue border */ + outline: none; + transition: border-color 0.25s ease; +} + +input:focus { + border-color: #14315a; +} + +button { + background-color: #1b3a6b; /* darker blue */ + color: white; + border: none; + cursor: pointer; + transition: background-color 0.25s ease; +} + +button:hover, +button:focus { + background-color: #14315a; + outline: none; +} + +#results { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.recipe { + border: 1px solid #ccc; + margin: 1rem; + padding: 1rem; + width: 200px; + background: #fff; + border-radius: 16px; /* rounder corners */ + box-shadow: 0 2px 6px rgba(27, 58, 107, 0.15); /* subtle shadow */ + transition: box-shadow 0.3s ease, transform 0.3s ease; + cursor: pointer; + text-align: center; +} + +.recipe:hover, +.recipe:focus { + box-shadow: 0 6px 20px rgba(27, 58, 107, 0.4); + transform: translateY(-4px); + outline: none; +} + +.recipe h3 { + color: #1b3a6b; + margin: 0.5rem 0 0.8rem; +} + +.recipe img { + max-width: 100%; + border-radius: 12px; /* round image corners */ +} + +/* Modal styles */ +.modal { + display: none; + position: fixed; + z-index: 999; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.6); +} + +.modal-content { + background-color: #fff; + margin: 10% auto; + padding: 2rem; + border: 1px solid #888; + width: 80%; + max-width: 600px; + position: relative; + border-radius: 12px; +} + +.close { + color: #aaa; + position: absolute; + top: 10px; + right: 20px; + font-size: 28px; + font-weight: bold; + cursor: pointer; + transition: color 0.2s ease; +} + +.close:hover, +.close:focus { + color: #1b3a6b; + outline: none; +} + +.veg-badge { + display: inline-block; + margin-top: 6px; + padding: 3px 8px; + background-color: #4caf50; + color: white; + font-weight: 600; + border-radius: 12px; + font-size: 0.8rem; + user-select: none; +}