Building a Recipe Web App
Using HTML, CSS and Javascript
In this article, we would create a functional application that allows individuals to search for food recipes. To get these recipes, we have to make use of an external API.
API stands for application programming interface, which is a set of definitions and protocols for building and integrating application software.
You use an API every time you use an application like Twitter, WhatsApp, or the weather forecast app on your device. The API functions as an intermediary between the user’s web page/ application and the web server. When you make an API call, a request is made to the server and you will receive an appropriate response.
HTML SetUp
We would create a header section that houses our logo and the tags for the page header. An input tag and a search tag with classes would come in handy during our page styling with CSS and DOM Manipulation with JavaScript.
An empty div is created which would hold our data fetched. We also create multiple images and details in the main section of the page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Recipes App</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css"
/>
<link rel="stylesheet" href="styles.css" />
<script src="javascript.js" defer></script>
</head>
<body>
<div class="mobile-container">
<header>
<input type="text" id="search-term" />
<button id="search"><i class="fas fa-search"></i></button>
</header>
<div class="fav-container">
<h3>Favorite Meals</h3>
<ul class="fav-meals" id="fav-meals"></ul>
</div>
<div class="meals" id="meals"></div>
</div>
<div class="popup-container hidden" id="meal-popup">
<div class="popup">
<button id="close-popup" class="close-popup">
<i class="fas fa-times"></i>
</button>
<div class="meal-info" id="meal-info"></div>
</div>
</div>
</body>
</html>
CSS Styling
To make our page more appealing and beautiful, we add some basic stylings to the HTML page. Do not forget to add the link to the CSS file to the HTML file created above using the link tag.
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;400;600&display=swap");
* {
box-sizing: border-box;
}
body {
background: #ece9e6;
background: -webkit-linear-gradient(to right, #ffffff, #ece9e6);
background: linear-gradient(to right, #ffffff, #ece9e6);
display: flex;
align-items: center;
justify-content: center;
font-family: "Poppins", sans-serif;
margin: 0;
min-height: 100vh;
}
img {
max-width: 100%;
}
.mobile-container {
background-color: #fff;
box-shadow: 0 0 10px 2px #3333331a;
border-radius: 3px;
overflow: hidden;
width: 400px;
}
header {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
header input {
background-color: #eee;
border: none;
border-radius: 3px;
font-family: inherit;
padding: 0.5rem 1rem;
}
header button {
background-color: transparent;
border: none;
color: rgb(138, 129, 141);
font-size: 1.5rem;
margin-left: 10px;
}
.fav-container {
background-color: rgb(243, 225, 255);
padding: 0.25rem 1rem;
text-align: center;
}
.fav-meals {
display: flex;
flex-wrap: wrap;
justify-content: center;
list-style-type: none;
padding: 0;
}
.fav-meals li {
cursor: pointer;
position: relative;
margin: 5px;
width: 75px;
}
.fav-meals li .clear {
background-color: transparent;
border: none;
cursor: pointer;
opacity: 0;
position: absolute;
top: -0.5rem;
right: -0.5rem;
font-size: 1.2rem;
}
.fav-meals li:hover .clear {
opacity: 1;
}
.fav-meals li img {
border: 2px solid #ffffff;
border-radius: 50%;
box-shadow: 0 0 10px 2px #3333331a;
object-fit: cover;
height: 70px;
width: 70px;
}
.fav-meals li span {
display: inline-block;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 75px;
}
.meal {
border-radius: 3px;
box-shadow: 0 0 10px 2px #3333331a;
cursor: pointer;
margin: 1.5rem;
overflow: hidden;
}
.meal-header {
position: relative;
}
.meal-header .random {
position: absolute;
top: 1rem;
background-color: #fff;
padding: 0.25rem 1rem;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
.meal-header img {
object-fit: cover;
height: 250px;
width: 100%;
}
.meal-body {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
}
.meal-body h4 {
margin: 0;
}
.meal-body .fav-btn {
border: none;
background-color: transparent;
color: rgb(197, 188, 188);
cursor: pointer;
font-size: 1.2rem;
}
.meal-body .fav-btn.active {
color: rebeccapurple;
}
.popup-container {
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.popup-container.hidden {
opacity: 0;
pointer-events: none;
}
.popup {
background-color: #fff;
border-radius: 5px;
padding: 0 2rem;
position: relative;
overflow: auto;
max-height: 100vh;
max-width: 600px;
width: 100%;
}
.popup .close-popup {
background-color: transparent;
border: none;
cursor: pointer;
font-size: 1.5rem;
position: absolute;
top: 1rem;
right: 1rem;
}
.meal-info h1 {
text-align: center;
}
JavaScript
This is where the real fun begins. We would do this together, one step at a time. We would select the ‘button’ and the ‘recipe’ class and save them to a variable ‘button’ and ‘recipe text’ respectively
Using the addeventlistener method, we would listen for a click and then execute a function. In this case, when the search button is clicked, it fetches the API. This can be easily achieved using Fetch or Axios. Fetch and Axios are very similar in functionality.
I pass the URL to fetch and set the .then() callback to console the data that gets returned from the request.
We would use the map() method to iterate and modify its elements using a callback function. The element argument in the map method is named ‘food’, feel free to give any name of your choice. The callback function will then be executed on each of the array's elements. For each of the elements, the block of code in the return is executed. In the return block, we are accessing the key of every element.
const mealsEl = document.getElementById("meals");
const favoriteContainer = document.getElementById("fav-meals");
const mealPopup = document.getElementById("meal-popup");
const mealInfoEl = document.getElementById("meal-info");
const popupCloseBtn = document.getElementById("close-popup");
const searchTerm = document.getElementById("search-term");
const searchBtn = document.getElementById("search");
getRandomMeal();
fetchFavMeals();
async function getRandomMeal() {
const resp = await fetch(
"https://www.themealdb.com/api/json/v1/1/random.php"
);
const respData = await resp.json();
const randomMeal = respData.meals[0];
addMeal(randomMeal, true);
}
async function getMealById(id) {
const resp = await fetch(
"https://www.themealdb.com/api/json/v1/1/lookup.php?i=" + id
);
const respData = await resp.json();
const meal = respData.meals[0];
return meal;
}
async function getMealsBySearch(term) {
const resp = await fetch(
"https://www.themealdb.com/api/json/v1/1/search.php?s=" + term
);
const respData = await resp.json();
const meals = respData.meals;
return meals;
}
function addMeal(mealData, random = false) {
console.log(mealData);
const meal = document.createElement("div");
meal.classList.add("meal");
meal.innerHTML = `
<div class="meal-header">
${
random
? `
<span class="random"> Random Recipe </span>`
: ""
}
<img
src="${mealData.strMealThumb}"
alt="${mealData.strMeal}"
/>
</div>
<div class="meal-body">
<h4>${mealData.strMeal}</h4>
<button class="fav-btn">
<i class="fas fa-heart"></i>
</button>
</div>
`;
const btn = meal.querySelector(".meal-body .fav-btn");
btn.addEventListener("click", () => {
if (btn.classList.contains("active")) {
removeMealLS(mealData.idMeal);
btn.classList.remove("active");
} else {
addMealLS(mealData.idMeal);
btn.classList.add("active");
}
fetchFavMeals();
});
meal.addEventListener("click", () => {
showMealInfo(mealData);
});
mealsEl.appendChild(meal);
}
function addMealLS(mealId) {
const mealIds = getMealsLS();
localStorage.setItem("mealIds", JSON.stringify([...mealIds, mealId]));
}
function removeMealLS(mealId) {
const mealIds = getMealsLS();
localStorage.setItem(
"mealIds",
JSON.stringify(mealIds.filter((id) => id !== mealId))
);
}
function getMealsLS() {
const mealIds = JSON.parse(localStorage.getItem("mealIds"));
return mealIds === null ? [] : mealIds;
}
async function fetchFavMeals() {
// clean the container
favoriteContainer.innerHTML = "";
const mealIds = getMealsLS();
for (let i = 0; i < mealIds.length; i++) {
const mealId = mealIds[i];
meal = await getMealById(mealId);
addMealFav(meal);
}
}
function addMealFav(mealData) {
const favMeal = document.createElement("li");
favMeal.innerHTML = `
<img
src="${mealData.strMealThumb}"
alt="${mealData.strMeal}"
/><span>${mealData.strMeal}</span>
<button class="clear"><i class="fas fa-window-close"></i></button>
`;
const btn = favMeal.querySelector(".clear");
btn.addEventListener("click", () => {
removeMealLS(mealData.idMeal);
fetchFavMeals();
});
favMeal.addEventListener("click", () => {
showMealInfo(mealData);
});
favoriteContainer.appendChild(favMeal);
}
function showMealInfo(mealData) {
// clean it up
mealInfoEl.innerHTML = "";
// update the Meal info
const mealEl = document.createElement("div");
const ingredients = [];
// get ingredients and measures
for (let i = 1; i <= 20; i++) {
if (mealData["strIngredient" + i]) {
ingredients.push(
`${mealData["strIngredient" + i]} - ${
mealData["strMeasure" + i]
}`
);
} else {
break;
}
}
mealEl.innerHTML = `
<h1>${mealData.strMeal}</h1>
<img
src="${mealData.strMealThumb}"
alt="${mealData.strMeal}"
/>
<p>
${mealData.strInstructions}
</p>
<h3>Ingredients:</h3>
<ul>
${ingredients
.map(
(ing) => `
<li>${ing}</li>
`
)
.join("")}
</ul>
`;
mealInfoEl.appendChild(mealEl);
// show the popup
mealPopup.classList.remove("hidden");
}
searchBtn.addEventListener("click", async () => {
// clean container
mealsEl.innerHTML = "";
const search = searchTerm.value;
const meals = await getMealsBySearch(search);
if (meals) {
meals.forEach((meal) => {
addMeal(meal);
});
}
});
popupCloseBtn.addEventListener("click", () => {
mealPopup.classList.add("hidden");
});
with this, you should able to get your recipe web app