Skip to content

Commit dee2c51

Browse files
committed
Add RecipeFinder mini project
1 parent 27ee633 commit dee2c51

File tree

6 files changed

+353
-0
lines changed

6 files changed

+353
-0
lines changed

RecipeFinder/shivt-F5/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# RecipeFinder
2+
3+
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.
4+
5+
![Screenshot](./screenshot.png)
6+
7+
---
8+
9+
##Features
10+
11+
- Search recipes by one or more ingredients
12+
- Displays image cards of matching recipes
13+
- Click any card to view full instructions and ingredient list
14+
- Vegetarian-friendly: automatically flags recipes
15+
- Responsive layout and smooth user experience
16+
- Enter and Escape key shortcuts for quick interaction
17+
18+
---
19+
20+
##Tech Stack
21+
22+
- *Language*: HTML, CSS, JavaScript
23+
- *API Used*: [TheMealDB](https://www.themealdb.com/)
24+
25+
---
26+
27+
##API Information
28+
29+
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:
30+
31+
- `https://www.themealdb.com/api/json/v1/1/filter.php?i=ingredient`
32+
- `https://www.themealdb.com/api/json/v1/1/lookup.php?i=mealID`
33+
34+
---
35+
36+
##How to Run Locally
37+
- Clone the repository, eg. in bash:
38+
git clone https://github.com/your-username/javascript-mini-projects.git
39+
cd javascript-mini-projects/RecipeFinder/shivt-F5
40+
- Open the app:
41+
Open RecipeFinder.html in your browser
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7+
<title>Recipe Finder</title>
8+
<link rel="stylesheet" href="style.css" />
9+
</head>
10+
11+
<body>
12+
<div class="container">
13+
<h1>Recipe Finder</h1>
14+
<h4>For multiple ingredients, separate keywords by space.</h4>
15+
<h4>If no recipes found, try searching ingredient in plural form.</h4>
16+
<input type="text" id="ingredient" placeholder="ingredient(s)" />
17+
<button id="searchBtn">Search</button>
18+
<div id="results"></div>
19+
</div>
20+
<div id="recipeModal" class="modal">
21+
<div class="modal-content">
22+
<span id="closeModal" class="close">&times;</span>
23+
<div id="modalBody"></div>
24+
</div>
25+
</div>
26+
<script src="script.js"></script>
27+
</body>
28+
29+
</html>
1.36 MB
Loading
486 KB
Loading

RecipeFinder/shivt-F5/script.js

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
const searchBtn = document.getElementById("searchBtn");
2+
const ingredientInput = document.getElementById("ingredient");
3+
const resultsDiv = document.getElementById("results");
4+
const modal = document.getElementById("recipeModal");
5+
const modalBody = document.getElementById("modalBody");
6+
const closeModal = document.getElementById("closeModal");
7+
8+
function doSearch() {
9+
const input = ingredientInput.value.trim().toLowerCase();
10+
const ingredients = input.split(/\s+/);
11+
resultsDiv.innerHTML = "";
12+
13+
if (!ingredients[0]) {
14+
resultsDiv.innerHTML = "<p>Please enter at least one ingredient.</p>";
15+
return;
16+
}
17+
18+
resultsDiv.innerHTML = "<p>Searching recipes...</p>";
19+
20+
Promise.all(
21+
ingredients.map((ing) =>
22+
fetch(`https://www.themealdb.com/api/json/v1/1/filter.php?i=${ing}`)
23+
.then((res) => res.json())
24+
.then((data) => data.meals || [])
25+
)
26+
)
27+
.then((results) => {
28+
if (results.some((arr) => arr.length === 0)) {
29+
resultsDiv.innerHTML = "<p>No recipes found with those ingredients.</p>";
30+
return;
31+
}
32+
33+
const commonMeals = results.reduce((acc, curr) => {
34+
const currIds = curr.map((m) => m.idMeal);
35+
return acc.filter((m) => currIds.includes(m.idMeal));
36+
}, results[0]);
37+
38+
if (!commonMeals.length) {
39+
resultsDiv.innerHTML = "<p>No recipes found with those ingredients.</p>";
40+
return;
41+
}
42+
43+
resultsDiv.innerHTML = "<p>Loading recipe details...</p>";
44+
45+
return loadMealDetailsWithVegFlag(commonMeals);
46+
})
47+
.then((mealsWithVegInfo) => {
48+
if (!mealsWithVegInfo) return;
49+
50+
resultsDiv.innerHTML = "";
51+
mealsWithVegInfo.forEach(({ mealDetails, isVegetarian }) => {
52+
const card = document.createElement("div");
53+
card.className = "recipe";
54+
card.innerHTML = `
55+
<h3>${mealDetails.strMeal}</h3>
56+
<img src="${mealDetails.strMealThumb}" alt="${mealDetails.strMeal}" />
57+
${isVegetarian ? '<span class="veg-badge">🌱 Vegetarian</span>' : ''}
58+
`;
59+
card.addEventListener("click", () => showRecipe(mealDetails.idMeal));
60+
resultsDiv.appendChild(card);
61+
});
62+
})
63+
.catch((err) => {
64+
console.error(err);
65+
resultsDiv.innerHTML = "<p>Error loading recipe details.</p>";
66+
});
67+
}
68+
69+
function loadMealDetailsWithVegFlag(meals) {
70+
const meatKeywords = [
71+
"chicken", "beef", "egg", "eggs", "prawns", "pork", "fish", "shrimp", "meat",
72+
"bacon", "ham", "lamb", "turkey", "anchovy", "crab", "duck", "salmon",
73+
"sausage", "veal", "venison", "shellfish", "octopus", "squid"
74+
];
75+
76+
return Promise.all(
77+
meals.map((meal) =>
78+
fetch(`https://www.themealdb.com/api/json/v1/1/lookup.php?i=${meal.idMeal}`)
79+
.then((res) => res.json())
80+
.then((data) => {
81+
const mealDetails = data.meals[0];
82+
let isVegetarian = true;
83+
84+
for (let i = 1; i <= 20; i++) {
85+
const ing = mealDetails[`strIngredient${i}`];
86+
if (ing && meatKeywords.some((keyword) => ing.toLowerCase().includes(keyword))) {
87+
isVegetarian = false;
88+
break;
89+
}
90+
}
91+
92+
return { mealDetails, isVegetarian };
93+
})
94+
)
95+
);
96+
}
97+
98+
function showRecipe(idMeal) {
99+
fetch(`https://www.themealdb.com/api/json/v1/1/lookup.php?i=${idMeal}`)
100+
.then((res) => res.json())
101+
.then((data) => {
102+
const meal = data.meals[0];
103+
let ingredients = "<ul>";
104+
105+
for (let i = 1; i <= 20; i++) {
106+
const ing = meal[`strIngredient${i}`];
107+
const measure = meal[`strMeasure${i}`];
108+
if (ing && ing.trim()) {
109+
ingredients += `<li>${measure} ${ing}</li>`;
110+
}
111+
}
112+
113+
ingredients += "</ul>";
114+
115+
modalBody.innerHTML = `
116+
<h2>${meal.strMeal}</h2>
117+
<img src="${meal.strMealThumb}" alt="${meal.strMeal}" style="max-width: 100%; margin: 1rem 0;" />
118+
<h4>Ingredients:</h4>
119+
${ingredients}
120+
<h4>Instructions:</h4>
121+
<p>${meal.strInstructions}</p>
122+
`;
123+
124+
modal.style.display = "block";
125+
ingredientInput.focus();
126+
})
127+
.catch((err) => {
128+
console.error(err);
129+
modalBody.innerHTML = "<p>Error loading recipe details.</p>";
130+
modal.style.display = "block";
131+
});
132+
}
133+
134+
searchBtn.addEventListener("click", doSearch);
135+
136+
ingredientInput.addEventListener("keydown", (e) => {
137+
if (e.key === "Enter") {
138+
doSearch();
139+
}
140+
});
141+
142+
closeModal.addEventListener("click", () => {
143+
modal.style.display = "none";
144+
});
145+
146+
window.addEventListener("click", (e) => {
147+
if (e.target === modal) {
148+
modal.style.display = "none";
149+
}
150+
});
151+
152+
window.addEventListener("keydown", (e) => {
153+
if (e.key === "Escape" && modal.style.display === "block") {
154+
modal.style.display = "none";
155+
}
156+
});

RecipeFinder/shivt-F5/style.css

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
body {
2+
font-family: Arial, sans-serif;
3+
text-align: center;
4+
background: #f8f8f8;
5+
color: #333;
6+
}
7+
8+
.container {
9+
padding: 2rem;
10+
}
11+
12+
input, button {
13+
padding: 0.5rem;
14+
margin: 1rem;
15+
font-size: 1rem;
16+
border-radius: 6px;
17+
border: 1.5px solid #1b3a6b; /* darker blue border */
18+
outline: none;
19+
transition: border-color 0.25s ease;
20+
}
21+
22+
input:focus {
23+
border-color: #14315a;
24+
}
25+
26+
button {
27+
background-color: #1b3a6b; /* darker blue */
28+
color: white;
29+
border: none;
30+
cursor: pointer;
31+
transition: background-color 0.25s ease;
32+
}
33+
34+
button:hover,
35+
button:focus {
36+
background-color: #14315a;
37+
outline: none;
38+
}
39+
40+
#results {
41+
display: flex;
42+
flex-wrap: wrap;
43+
justify-content: center;
44+
}
45+
46+
.recipe {
47+
border: 1px solid #ccc;
48+
margin: 1rem;
49+
padding: 1rem;
50+
width: 200px;
51+
background: #fff;
52+
border-radius: 16px; /* rounder corners */
53+
box-shadow: 0 2px 6px rgba(27, 58, 107, 0.15); /* subtle shadow */
54+
transition: box-shadow 0.3s ease, transform 0.3s ease;
55+
cursor: pointer;
56+
text-align: center;
57+
}
58+
59+
.recipe:hover,
60+
.recipe:focus {
61+
box-shadow: 0 6px 20px rgba(27, 58, 107, 0.4);
62+
transform: translateY(-4px);
63+
outline: none;
64+
}
65+
66+
.recipe h3 {
67+
color: #1b3a6b;
68+
margin: 0.5rem 0 0.8rem;
69+
}
70+
71+
.recipe img {
72+
max-width: 100%;
73+
border-radius: 12px; /* round image corners */
74+
}
75+
76+
/* Modal styles */
77+
.modal {
78+
display: none;
79+
position: fixed;
80+
z-index: 999;
81+
left: 0;
82+
top: 0;
83+
width: 100%;
84+
height: 100%;
85+
overflow: auto;
86+
background-color: rgba(0,0,0,0.6);
87+
}
88+
89+
.modal-content {
90+
background-color: #fff;
91+
margin: 10% auto;
92+
padding: 2rem;
93+
border: 1px solid #888;
94+
width: 80%;
95+
max-width: 600px;
96+
position: relative;
97+
border-radius: 12px;
98+
}
99+
100+
.close {
101+
color: #aaa;
102+
position: absolute;
103+
top: 10px;
104+
right: 20px;
105+
font-size: 28px;
106+
font-weight: bold;
107+
cursor: pointer;
108+
transition: color 0.2s ease;
109+
}
110+
111+
.close:hover,
112+
.close:focus {
113+
color: #1b3a6b;
114+
outline: none;
115+
}
116+
117+
.veg-badge {
118+
display: inline-block;
119+
margin-top: 6px;
120+
padding: 3px 8px;
121+
background-color: #4caf50;
122+
color: white;
123+
font-weight: 600;
124+
border-radius: 12px;
125+
font-size: 0.8rem;
126+
user-select: none;
127+
}

0 commit comments

Comments
 (0)