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. + +![Screenshot](./screenshot.png) + +--- + +##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 @@ + + + + + + + Recipe Finder + + + + +
+

Recipe Finder

+

For multiple ingredients, separate keywords by space.

+

If no recipes found, try searching ingredient in plural form.

+ + +
+
+ + + + + diff --git a/RecipeFinder/shivt-F5/RecipeFinderGIF.gif b/RecipeFinder/shivt-F5/RecipeFinderGIF.gif new file mode 100644 index 000000000..821294680 Binary files /dev/null and b/RecipeFinder/shivt-F5/RecipeFinderGIF.gif differ diff --git a/RecipeFinder/shivt-F5/screenshot.png b/RecipeFinder/shivt-F5/screenshot.png new file mode 100644 index 000000000..77f37bb0d Binary files /dev/null and b/RecipeFinder/shivt-F5/screenshot.png differ diff --git a/RecipeFinder/shivt-F5/script.js b/RecipeFinder/shivt-F5/script.js new file mode 100644 index 000000000..ec5ff9a5c --- /dev/null +++ b/RecipeFinder/shivt-F5/script.js @@ -0,0 +1,156 @@ +const searchBtn = document.getElementById("searchBtn"); +const ingredientInput = document.getElementById("ingredient"); +const resultsDiv = document.getElementById("results"); +const modal = document.getElementById("recipeModal"); +const modalBody = document.getElementById("modalBody"); +const closeModal = document.getElementById("closeModal"); + +function doSearch() { + const input = ingredientInput.value.trim().toLowerCase(); + const ingredients = input.split(/\s+/); + resultsDiv.innerHTML = ""; + + if (!ingredients[0]) { + resultsDiv.innerHTML = "

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 = ` +

${mealDetails.strMeal}

+ ${mealDetails.strMeal} + ${isVegetarian ? '🌱 Vegetarian' : ''} + `; + card.addEventListener("click", () => showRecipe(mealDetails.idMeal)); + resultsDiv.appendChild(card); + }); + }) + .catch((err) => { + console.error(err); + resultsDiv.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 = ""; + + modalBody.innerHTML = ` +

${meal.strMeal}

+ ${meal.strMeal} +

Ingredients:

+ ${ingredients} +

Instructions:

+

${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; +}