Building a Recipe Finder Application with HTML, CSS, and JavaScript

CODES CRACKER
0

 


Introduction:

In this blog post, we will guide you through the process of building a Recipe Finder application using HTML, CSS, and JavaScript. The Recipe Finder allows users to search for recipes based on ingredients or dish names, providing them with a vast collection of culinary options. Leveraging the power of a public API, we will fetch recipe data and display it in an organized and visually appealing manner. Let's dive into the details and create this exciting application step by step.

Section 1: Setting up the HTML Structure

The first step in building our Recipe Finder application is to set up the HTML structure. We'll create a container div to hold the entire application, a heading for the application title, an input field for users to enter their search query, and a search button to initiate the search functionality. Additionally, we'll include a results container to display the search results.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    <link rel="stylesheet" href="styles.css">
    <script src="script.js" defer></script>
    <title>Recipe App</title>
    <!-- We're going to use a meal API ===> https://www.themealdb.com/ -->
    <!-- Font Awesome for the icons -->
</head>
<body>
    <div class="container">
        <div class="card">
            <div class="top">
                <span class="light-dark-mode">
                    <i class="fa-solid fa-sun"></i>
                </span>
                <span class="search-container">
                    <input class="search-input" type="text" placeholder="Search meal...">
                    <span class="search-icon">
                        <i class="fa-solid fa-magnifying-glass"></i>
                    </span>
                </span>
            </div>
            <div class="fav-meals-container">
                <h2>Favorite Meals</h2>
                <div class="fav-meals">
                   
                </div>
            </div>
        </div>
        <div class="meals-container">
            <h2>Random Meal</h2>
            <div class="meal">
               
            </div>
        </div>
    </div>

    <div class="pop-up-container">
        <div class="pop-up">
            <i class="fa-solid fa-x"></i>
            <div class="pop-up-inner">
               
            </div>
        </div>
    </div>
</body>
</html>


Section 2: Styling with CSS

To make our Recipe Finder application visually appealing, we'll apply CSS styles to enhance its appearance. We'll set the font family, background color, and box shadow for the container. The search container and search results will have specific styles to improve the user experience.

@import url(https://fonts.googleapis.com/css?family=Poppins:100,100italic,200,200italic,300,300italic,regular,italic,500,500italic,600,600italic,700,700italic,800,800italic,900,900italic);
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Poppins', sans-serif;
}
body {
    display: grid;
    place-items: center;
    min-height: 100vh;
    background-color: var(--primary-clr);
    padding: 3rem 0;
}
:root {
    --primary-clr: #161616;
    --primary-rgba-7: rgba(0, 0, 0, .4);

    --secondary-clr: #fff9f9;
    --secondary-rgba-3: rgba(255, 255, 255, .3);
}
.light-theme {
    --primary-clr: #fff9f9;
    --primary-rgba-7: rgba(255, 255, 255, .4);

    --secondary-clr: #161616;
    --secondary-rgba-3: rgba(0, 0, 0, .3);
}

.card {
    width: clamp(300px, 92vw, 400px);
    background-color: var(--primary-clr);
    box-shadow: 0 0 .5rem var(--secondary-rgba-3);
    border-radius: 1rem;
    margin: 0 auto;
    overflow: hidden;
}
.top {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1.5rem 1rem;
}
.light-dark-mode {
    width: 40px;
    aspect-ratio: 1;
    box-shadow: 0 0 .3rem var(--secondary-rgba-3);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--secondary-clr);
    font-size: 1rem;
    border-radius: 50%;
    cursor: pointer;
    transition: 200ms ease-in-out background-color,
                200ms ease-in-out color;
}
.light-dark-mode:active {
    transform: scale(.8);
}
.light-dark-mode:hover {
    background-color: var(--secondary-clr);
    color: var(--primary-clr);
}
.search-container {
    width: 250px;
    height: 40px;
    position: relative;
}
.search-container input {
    width: 40px;
    height: 100%;
    position: absolute;
    top: 0;
    right: 0;
    font-size: 1rem;
    padding: 0;
    outline: none;
    border-radius: 100vw;
    border: none;
    box-shadow: 0 0 .3rem var(--secondary-rgba-3);
    background-color: transparent;
    color: var(--secondary-clr);
    transition: 200ms ease-in-out width,
                200ms ease-in-out padding;
}
.search-icon {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    background-color: var(--primary-clr);
    box-shadow: 0 0 .3rem var(--secondary-rgba-3);
    height: 40px;
    aspect-ratio: 1;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--secondary-clr);
    cursor: pointer;
    transition: 200ms ease-in-out background-color,
                200ms ease-in-out color;
}
.search-icon:hover {
    background-color: var(--secondary-clr);
    color: var(--primary-clr);
}
.search-icon:active {
    transform: scale(.8) translateY(-50%);
}
.search-container:hover input,
.search-container input:focus {
    width: 100%;
    padding: 0 4rem 0 1rem;
}

.fav-meals-container {
    text-align: center;
    color: var(--secondary-clr);
    box-shadow: 0 0 .3rem var(--secondary-rgba-3);
    padding: 1rem;
}
.fav-meals {
    display: flex;
    align-items: center;
    column-gap: 1rem;
    width: auto;
    overflow-x: scroll;
    padding: .5rem;
    padding-bottom: 1rem;
}
.fav-meals-container h2 {
    font-weight: 400;
    text-shadow: 0 0 .4rem var(--secondary-rgba-3);
    margin-bottom: 1rem;
}
.single {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 80px;
    cursor: pointer;
    border-radius: .5rem;
    box-shadow: 0 0 .5rem var(--secondary-rgba-3);
    position: relative;
    transition: 200ms ease-in-out background-color;
}
.single .top {
    display: flex;
    align-items: center;
    flex-direction: column;
    justify-content: center;
    padding: 0;
}
.img-container {
    width: 60px;
    aspect-ratio: 1;
    margin-top: 0.5rem;
    border-radius: 50%;
    overflow: hidden;
    box-sizing: 0 0 .4rem var(--secondary-rgba-3);
}
.img-container img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.text {
    margin: 0.5rem 0;
}
.text p {
    width: 70px;
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
    font-size: .8rem;
    text-shadow: 0 0 .3rem var(--secondary-rgba-3);
    padding: 0 .5rem;
    user-select: none;
    transition: 200ms ease-in-out color;
}
.fa-x {
    color: var(--secondary-clr);
    background-color: var(--primary-clr);
    border-bottom-left-radius: .5rem;
    border-bottom-right-radius: .5rem;
    width: 100%;
    padding: .5rem 0;
}
.single:hover {
    background-color: var(--secondary-clr);
}
.single:hover .img-container {
    box-shadow: 0 0 .4rem var(--primary-rgba-7);
}
.single:hover .text p {
    color: var(--primary-clr);
    text-shadow: 0 0 .2rem var(--primary-rgba-7);
}
::-webkit-scrollbar{
    width: 6px;
    height: 6px;
}
::-webkit-scrollbar-thumb {
    background-color: var(--secondary-rgba-3);
    border-radius: 100vw;
}

.meal {
    padding: 1rem;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 2rem;
}
.meals-container h2 {
    text-align: center;
    margin-top: 1rem;
    font-weight: 400;
    color: var(--secondary-clr);
    text-shadow: 0 0 .4rem var(--secondary-rgba-3);
}
.meal-card {
    width: clamp(300px, 60vw, 400px);
    box-shadow: 0 0 .4rem var(--secondary-rgba-3);
    border-radius: 1rem;
    overflow: hidden;
    margin-bottom: 2rem;
    transition: 200ms ease-in-out background-color;
}
.meal-card-img-container {
    width: 100%;
    height: 230px;
    cursor: pointer;
}
.meal-card-img-container img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.meal-name {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem;
    color: var(--secondary-clr);
    font-size: 1.1rem;
    text-shadow: 0 0 .3rem var(--secondary-rgba-3);
    transition: 200ms ease-in-out color,
                200ms ease-in-out text-shadow;
}
.meal-name i {
    color: #ff0000;
    text-shadow: none;
    cursor: pointer;
}
.meal-card:hover {
    background-color: var(--secondary-clr);
}
.meal-card:hover .meal-name {
    color: var(--primary-clr);
    text-shadow: 0 0 .2rem var(--primary-clr);
}

.pop-up-container {
    position: fixed;
    inset: 0;
    background-color: var(--primary-rgba-7);
    display: flex;
    align-items: center;
    justify-content: center;

    /* as default it'll be hidden */
    display: none;
}
.pop-up {
    width: clamp(200px, 92vw, 900px);
    max-height: 90vh;
    box-shadow: 0 0 .5rem var(--secondary-rgba-3);
    background-color: var(--primary-clr);
    color: var(--secondary-clr);
    padding: 4rem 2rem;
    border-radius: 1rem;
    overflow: hidden;
    position: relative;
}
.pop-up-inner {
    display: flex;
    gap: 2rem;
}
.pop-up i {
    position: absolute;
    right: 20px;
    top: 20px;
    width: auto;
    background-color: transparent;
    font-size: 1.3rem;
    cursor: pointer;
}
.pop-up .left i {
    display: none;
}
.right {
    overflow-y: scroll;
    max-height: 85vh;
    padding-right: 1rem;
    padding-bottom: 4rem;
}
.right div {
    margin-bottom: 2rem;
}
.right ul li {
    padding-right: 2rem;
    list-style-position: inside;
}
.right .meal-info,
.right ul {
    margin-top: 0.5rem;
}

@media only screen and (max-width: 800px) {
    .pop-up {
        overflow-y: scroll;
        padding: 0;
    }
    .pop-up-inner {
        flex-direction: column;
        align-items: center;
        padding: 3rem 0;
    }
    .right {
        padding: 0 1rem;
        max-height: 100%;
        overflow: hidden;
    }
    .right ul li {
        padding-right: 0;
    }
    ::-webkit-scrollbar {
        display: none;
    }
}


Section 3: Implementing JavaScript Functionality

Now it's time to add JavaScript functionality to our Recipe Finder application. We'll start by selecting the necessary HTML elements using JavaScript. We'll then add an event listener to the search button and implement the logic to fetch recipe data from the Spoonacular API based on the user's search query. Finally, we'll dynamically display the search results in the results container.

const mealEl_container = document.querySelector('.meal')
const fav_meals_container = document.querySelector('.fav-meals');

const search_input = document.querySelector('.search-input');
const search_icon = document.querySelector('.search-icon');

const popup_container = document.querySelector('.pop-up-container');
const close_popup_btn = document.querySelector('.pop-up > i');
const popup = document.querySelector('.pop-up-inner');

const lightDarkModeSpan = document.querySelector('.light-dark-mode');
const lightDarkModeIcon = document.querySelector('.light-dark-mode > i');

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 random_meal = respData.meals[0];
    console.log(random_meal);
    addMeal(random_meal)
}

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 (meal) {
    const meal_card = document.createElement('div');
    meal_card.classList.add('meal-card');
    meal_card.innerHTML = `
            <div class="meal-card-img-container">
                <img src="${meal.strMealThumb}">
            </div>
            <div class="meal-name">
                <p>${meal.strMeal}</p>
                <i class="fa-regular fa-heart"></i>
            </div>
    `

    const btn = meal_card.querySelector('.fa-heart');
    btn.addEventListener('click', () => {
        if (btn.classList.contains('fa-regular')) {
            btn.setAttribute('class', 'fa-solid fa-heart')
            addMealLS(meal.idMeal)
        } else {
            btn.setAttribute('class', 'fa-regular fa-heart')
            removeMealLS(meal.idMeal)
        }
        fetchFavMeals()
    })

    meal_card.firstChild.nextSibling.addEventListener('click', () => {
        showMealPopup(meal)
    })

    mealEl_container.appendChild(meal_card)
}

function addMealLS (mealID) {
    const mealIds = getMealLS()
    localStorage.setItem('mealIds', JSON.stringify([...mealIds, mealID]))
}

function removeMealLS (mealID) {
    const mealIds = getMealLS()
    localStorage.setItem('mealIds', JSON.stringify(mealIds.filter(id => id !== mealID)))
}

function getMealLS () {
    const mealIds = JSON.parse(localStorage.getItem('mealIds'));

    return mealIds === null ? [] : mealIds
}

async function fetchFavMeals () {
    fav_meals_container.innerHTML = '';

    const mealsIds = getMealLS();
    const meals = [];
    for(let i = 0; i < mealsIds.length; i++) {
        const mealID = mealsIds[i];
        meal = await getMealById(mealID)
        addMealToFav(meal)
        meals.push(meal)
    }
}

function addMealToFav (meal) {
    const fav_meals = document.createElement('div');
    fav_meals.innerHTML = `
            <div class="single">
                <div class="top">
                    <div class="img-container">
                        <img src="${meal.strMealThumb}">
                    </div>
                    <div class="text">
                        <p>${meal.strMeal}</p>
                    </div>
                </div>
                <i class="fa-solid fa-x"></i>
            </div>
    `
    const x = fav_meals.querySelector('.fa-x');
    x.addEventListener('click', () => {
        removeMealLS(meal.idMeal)

        const heart_btns = document.querySelectorAll('.fa-heart');
        heart_btns.forEach(heart_btn => {
            heart_btn.setAttribute('class', 'fa-regular fa-heart');
        })

        fetchFavMeals()
    })

    fav_meals.firstChild.nextSibling.firstChild.nextSibling.addEventListener('click', () => {
        showMealPopup(meal)
    })

    fav_meals_container.appendChild(fav_meals)
}

search_icon.addEventListener('click', async () => {
    mealEl_container.innerHTML = '';
    const searchVal = search_input.value;
    const meals = await getMealsBySearch(searchVal)
    if (meals) {
        meals.forEach(meal => {
            addMeal(meal)
        })
        document.querySelector('.meals-container > h2').innerText = 'Search Results...'
    } else {
        document.querySelector('.meals-container > h2').innerText = 'No Meals Found'
        mealEl_container.innerHTML = '';
    }
})

close_popup_btn.addEventListener('click', () => {
    popup_container.style.display = 'none';
})
function showMealPopup (meal) {
    popup.innerHTML = ''

    const newPopup = document.createElement('div');
    newPopup.classList.add('pop-up-inner');

    const ingredients = [];
    for(let i = 1; i <= 20; i++) {
        if (meal[`strIngredient${i}`]) {
            ingredients.push(`${meal[`strIngredient${i}`]} - ${meal[`strMeasure${i}`]}`)
        } else {
            break
        }
    }

    newPopup.innerHTML = `
        <div class="left">
            <div class="meal-card">
                <div class="meal-card-img-container">
                    <img src="${meal.strMealThumb}" alt="">
                </div>
                <div class="meal-name">
                    <p>${meal.strMeal}</p>
                    <i class="fa-regular fa-heart"></i>
                </div>
            </div>
        </div>
        <div class="right">
            <div>
                <h2>Intructions</h2>
                <p class="meal-info">${meal.strInstructions}</p>
            </div>
            <div>
                <h2>Ingredients / Measures</h2>
                <ul>
                    ${ingredients.map(e => `<li>${e}</li>`).join('')}
                </ul>
            </div>
        </div>
    `
    popup.appendChild(newPopup);
    popup_container.style.display = 'flex';
}

lightDarkModeSpan.addEventListener('click', () => {
    if (lightDarkModeIcon.classList.contains('fa-sun')) {
        lightDarkModeIcon.setAttribute('class', 'fa-solid fa-moon')
    } else {
        lightDarkModeIcon.setAttribute('class', 'fa-solid fa-sun')
    }

    document.documentElement.classList.toggle('light-theme');
})


OUTPUT :



Conclusion:

Congratulations! You've successfully built a Recipe Finder application using HTML, CSS, and JavaScript. This application allows users to search for recipes based on ingredients or dish names, providing them with a wide range of culinary options. By leveraging the Spoonacular API, the Recipe Finder fetches recipe data and displays it in a visually appealing format. Feel free to customize and expand this application further to add more features and functionalities.

We hope you enjoyed building the Recipe Finder application. Stay tuned for more exciting coding projects and tutorials on our blog. If you have any questions or feedback, please leave a comment below. Happy cooking and coding!



Post a Comment

0Comments

Post a Comment (0)