Skip to content

Add RecipeFinder mini project #1196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
41 changes: 41 additions & 0 deletions RecipeFinder/shivt-F5/README.md
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions RecipeFinder/shivt-F5/RecipeFinder.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Recipe Finder</title>
<link rel="stylesheet" href="style.css" />
</head>

<body>
<div class="container">
<h1>Recipe Finder</h1>
<h4>For multiple ingredients, separate keywords by space.</h4>
<h4>If no recipes found, try searching ingredient in plural form.</h4>
<input type="text" id="ingredient" placeholder="ingredient(s)" />
<button id="searchBtn">Search</button>
<div id="results"></div>
</div>
<div id="recipeModal" class="modal">
<div class="modal-content">
<span id="closeModal" class="close">&times;</span>
<div id="modalBody"></div>
</div>
</div>
<script src="script.js"></script>
</body>

</html>
Binary file added RecipeFinder/shivt-F5/RecipeFinderGIF.gif
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 RecipeFinder/shivt-F5/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 156 additions & 0 deletions RecipeFinder/shivt-F5/script.js
Original file line number Diff line number Diff line change
@@ -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 = "<p>Please enter at least one ingredient.</p>";
return;
}

resultsDiv.innerHTML = "<p>Searching recipes...</p>";

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 = "<p>No recipes found with those ingredients.</p>";
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 = "<p>No recipes found with those ingredients.</p>";
return;
}

resultsDiv.innerHTML = "<p>Loading recipe details...</p>";

return loadMealDetailsWithVegFlag(commonMeals);
})
.then((mealsWithVegInfo) => {
if (!mealsWithVegInfo) return;

resultsDiv.innerHTML = "";
mealsWithVegInfo.forEach(({ mealDetails, isVegetarian }) => {
const card = document.createElement("div");
card.className = "recipe";
card.innerHTML = `
<h3>${mealDetails.strMeal}</h3>
<img src="${mealDetails.strMealThumb}" alt="${mealDetails.strMeal}" />
${isVegetarian ? '<span class="veg-badge">🌱 Vegetarian</span>' : ''}
`;
card.addEventListener("click", () => showRecipe(mealDetails.idMeal));
resultsDiv.appendChild(card);
});
})
.catch((err) => {
console.error(err);
resultsDiv.innerHTML = "<p>Error loading recipe details.</p>";
});
}

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 = "<ul>";

for (let i = 1; i <= 20; i++) {
const ing = meal[`strIngredient${i}`];
const measure = meal[`strMeasure${i}`];
if (ing && ing.trim()) {
ingredients += `<li>${measure} ${ing}</li>`;
}
}

ingredients += "</ul>";

modalBody.innerHTML = `
<h2>${meal.strMeal}</h2>
<img src="${meal.strMealThumb}" alt="${meal.strMeal}" style="max-width: 100%; margin: 1rem 0;" />
<h4>Ingredients:</h4>
${ingredients}
<h4>Instructions:</h4>
<p>${meal.strInstructions}</p>
`;

modal.style.display = "block";
ingredientInput.focus();
})
.catch((err) => {
console.error(err);
modalBody.innerHTML = "<p>Error loading recipe details.</p>";
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";
}
});
127 changes: 127 additions & 0 deletions RecipeFinder/shivt-F5/style.css
Original file line number Diff line number Diff line change
@@ -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;
}