Introduction
In this blog, we'll be building a Recipe Finder Website using React. This app allows users to search for their favorite recipes, view trending or new recipes, and save their favorite ones. We will leverage the Edamam API to fetch real-time recipe data and display it dynamically on the website.
Project Overview
The Recipe Finder allows users to:
- Search for recipes by name.
- View trending and newly added recipes.
- View detailed information about individual recipes.
- Add recipes to a favorites list and persist the data using
localStorage
.
Features
- Search Functionality: Users can search for recipes by entering a query.
- Trending Recipes: Displays currently trending recipes from the API.
- New Recipes: Displays the latest recipes from the API.
- Recipe Detail: Displays detailed information about a selected recipe.
- Favorites: Allows users to add recipes to a favorites list, which is saved locally.
Technologies Used
- React: For building the user interface.
- React Router: For navigation between different pages.
- Edamam API: For fetching recipes.
- CSS: For styling the application.
Project Structure
src/
│
├── components/
│ └── Navbar.js
│
├── pages/
│ ├── Home.js
│ ├── About.js
│ ├── Trending.js
│ ├── NewRecipe.js
│ ├── RecipeDetail.js
│ ├── Contact.js
│ └── Favorites.js
│
├── App.js
├── index.js
├── App.css
└── index.css
Installation
To run this project locally, follow these steps:
- Clone the repository:
git clone https://github.com/abhishekgurjar-in/recipe-finder.git
cd recipe-finder
- Install the dependencies:
npm install
- Start the React app:
npm start
Obtain your Edamam API credentials (API ID and API Key) from the Edamam website.
Add your API credentials inside the pages where API calls are made, such as
Home.js
,Trending.js
,NewRecipe.js
, andRecipeDetail.js
.
Usage
App.js
import React from "react";
import Navbar from "./components/Navbar";
import { Route, Routes } from "react-router-dom";
import "./App.css";
import Home from "./pages/Home";
import About from "./pages/About";
import Trending from "./pages/Trending";
import NewRecipe from "./pages/NewRecipe";
import RecipeDetail from "./pages/RecipeDetail";
import Contact from "./pages/Contact";
import Favorites from "./pages/Favorites";
const App = () => {
return (
<>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/trending" element={<Trending />} />
<Route path="/new-recipe" element={<NewRecipe />} />
<Route path="/new-recipe" element={<NewRecipe />} />
<Route path="/recipe/:id" element={<RecipeDetail />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact/>} />
<Route path="/favorites" element={<Favorites/>} />
</Routes>
<div className="footer">
<p>Made with ❤️ by Abhishek Gurjar</p>
</div>
</>
);
};
export default App;
Home.js
This is the main page where users can search for recipes using the Edamam API.
import React, { useState, useRef, useEffect } from "react";
import { IoSearch } from "react-icons/io5";
import { Link } from "react-router-dom";
const Home = () => {
const [query, setQuery] = useState("");
const [recipe, setRecipe] = useState([]);
const recipeSectionRef = useRef(null);
const API_ID = "2cbb7807";
const API_KEY = "17222f5be3577d4980d6ee3bb57e9f00";
const getRecipe = async () => {
if (!query) return; // Add a check to ensure the query is not empty
const response = await fetch(
`https://api.edamam.com/search?q=${query}&app_id=${API_ID}&app_key=${API_KEY}`
);
const data = await response.json();
setRecipe(data.hits);
console.log(data.hits);
};
// Use useEffect to detect changes in the recipe state and scroll to the recipe section
useEffect(() => {
if (recipe.length > 0 && recipeSectionRef.current) {
recipeSectionRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [recipe]);
// Handle key down event to trigger getRecipe on Enter key press
const handleKeyDown = (e) => {
if (e.key === "Enter") {
getRecipe();
}
};
return (
<div className="home">
<div className="home-main">
<div className="home-text">
<h1>Find your Favourite Recipe</h1>
</div>
<div className="input-box">
<span>
<input
type="text"
placeholder="Enter Recipe"
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown} // Add the onKeyDown event handler
/>
</span>
<IoSearch className="search-btn" onClick={getRecipe} />
</div>
</div>
<div ref={recipeSectionRef} className="recipes">
{recipe.map((item, index) => (
<div key={index} className="recipe">
<img
className="recipe-img"
src={item.recipe.image}
alt={item.recipe.label}
/>
<h2 className="label">{item.recipe.label}</h2>
<Link to={`/recipe/${item.recipe.uri.split("_")[1]}`}>
<button className="button">View Recipe</button>
</Link>
</div>
))}
</div>
</div>
);
};
export default Home;
Trending.js
This page fetches and displays trending recipes.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
const Trending = () => {
const [trendingRecipes, setTrendingRecipes] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const API_ID = "2cbb7807";
const API_KEY = "17222f5be3577d4980d6ee3bb57e9f00";
useEffect(() => {
const fetchTrendingRecipes = async () => {
try {
const response = await fetch(
`https://api.edamam.com/api/recipes/v2?type=public&q=trending&app_id=${API_ID}&app_key=${API_KEY}`
);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
setTrendingRecipes(data.hits);
setLoading(false);
} catch (error) {
setError("Failed to fetch trending recipes");
setLoading(false);
}
};
fetchTrendingRecipes();
}, []);
if (loading)
return (
<div className="loader-section">
<div className="loader"></div>
</div>
);
if (error) return <div>{error}</div>;
return (
<div className="trending-recipe">
<div className="trending-recipe-main">
<div className="trending-recipe-text">
<h1>Trending Recipes</h1>
</div>
</div>
<div className="recipes">
{trendingRecipes.map((item, index) => (
<div key={index} className="recipe">
<img
className="recipe-img"
src={item.recipe.image}
alt={item.recipe.label}
/>
<h2 className="label">{item.recipe.label}</h2>
<Link to={`/recipe/${item.recipe.uri.split("_")[1]}`}>
<button className="button">View Recipe</button>
</Link>
</div>
))}
</div>
</div>
);
};
export default Trending;
NewRecipe.js
This page fetches NewRecipe and displays New Recipes.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
const NewRecipe = () => {
const [newRecipes, setNewRecipes] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const API_ID = "2cbb7807";
const API_KEY = "17222f5be3577d4980d6ee3bb57e9f00";
useEffect(() => {
const fetchNewRecipes = async () => {
try {
const response = await fetch(
`https://api.edamam.com/api/recipes/v2?type=public&q=new&app_id=${API_ID}&app_key=${API_KEY}`
);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
setNewRecipes(data.hits);
setLoading(false);
} catch (error) {
setError("Failed to fetch new recipes");
setLoading(false);
}
};
fetchNewRecipes();
}, []);
if (loading)
return (
<div className="loader-section">
<div className="loader"></div>
</div>
);
if (error) return <div>{error}</div>;
return (
<div className="new-recipe">
<div className="new-recipe-main">
<div className="new-recipe-text">
<h1>New Recipes</h1>
</div>
</div>
<div className="recipes">
{newRecipes.map((item, index) => (
<div key={index} className="recipe">
<img
className="recipe-img"
src={item.recipe.image}
alt={item.recipe.label}
/>
<h2 className="label">{item.recipe.label}</h2>
<Link to={`/recipe/${item.recipe.uri.split("_")[1]}`}>
<button className="button">View Recipe</button>
</Link>
</div>
))}
</div>
</div>
);
};
export default NewRecipe;
Home.js
This page fetches and displays Home page and serached recipe.
import React, { useState, useRef, useEffect } from "react";
import { IoSearch } from "react-icons/io5";
import { Link } from "react-router-dom";
const Home = () => {
const [query, setQuery] = useState("");
const [recipe, setRecipe] = useState([]);
const recipeSectionRef = useRef(null);
const API_ID = "2cbb7807";
const API_KEY = "17222f5be3577d4980d6ee3bb57e9f00";
const getRecipe = async () => {
if (!query) return; // Add a check to ensure the query is not empty
const response = await fetch(
`https://api.edamam.com/search?q=${query}&app_id=${API_ID}&app_key=${API_KEY}`
);
const data = await response.json();
setRecipe(data.hits);
console.log(data.hits);
};
// Use useEffect to detect changes in the recipe state and scroll to the recipe section
useEffect(() => {
if (recipe.length > 0 && recipeSectionRef.current) {
recipeSectionRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [recipe]);
// Handle key down event to trigger getRecipe on Enter key press
const handleKeyDown = (e) => {
if (e.key === "Enter") {
getRecipe();
}
};
return (
<div className="home">
<div className="home-main">
<div className="home-text">
<h1>Find your Favourite Recipe</h1>
</div>
<div className="input-box">
<span>
<input
type="text"
placeholder="Enter Recipe"
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown} // Add the onKeyDown event handler
/>
</span>
<IoSearch className="search-btn" onClick={getRecipe} />
</div>
</div>
<div ref={recipeSectionRef} className="recipes">
{recipe.map((item, index) => (
<div key={index} className="recipe">
<img
className="recipe-img"
src={item.recipe.image}
alt={item.recipe.label}
/>
<h2 className="label">{item.recipe.label}</h2>
<Link to={`/recipe/${item.recipe.uri.split("_")[1]}`}>
<button className="button">View Recipe</button>
</Link>
</div>
))}
</div>
</div>
);
};
export default Home;
Favorites.js
This page displays Favorites recipes.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
const Favorites = () => {
const [favorites, setFavorites] = useState([]);
useEffect(() => {
const savedFavorites = JSON.parse(localStorage.getItem("favorites")) || [];
setFavorites(savedFavorites);
}, []);
if (favorites.length === 0) {
return <div>No favorite recipes found.</div>;
}
return (
<div className="favorites-page ">
<div className="favorite-recipes-text">
<h1>Favorite Recipes</h1>
</div>
<ul className="recipes">
{favorites.map((recipe) => (
<div className="recipe">
<img className="recipe-img" src={recipe.image} alt={recipe.label} />
<h2 className="label">{recipe.label}</h2>
<Link to={`/recipe/${recipe.uri.split("_")[1]}`}>
<button className="button">View Recipe</button>
</Link>
</div>
))}
</ul>
</div>
);
};
export default Favorites;
RecipeDetail.js
This page displays of the recipes.
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
const RecipeDetail = () => {
const { id } = useParams(); // Use React Router to get the recipe ID from the URL
const [recipe, setRecipe] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [favorites, setFavorites] = useState([]);
const API_ID = "2cbb7807";
const API_KEY = "17222f5be3577d4980d6ee3bb57e9f00";
useEffect(() => {
const fetchRecipeDetail = async () => {
try {
const response = await fetch(
`https://api.edamam.com/api/recipes/v2/${id}?type=public&app_id=${API_ID}&app_key=${API_KEY}`
);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
setRecipe(data.recipe);
setLoading(false);
} catch (error) {
setError("Failed to fetch recipe details");
setLoading(false);
}
};
fetchRecipeDetail();
}, [id]);
useEffect(() => {
const savedFavorites = JSON.parse(localStorage.getItem("favorites")) || [];
setFavorites(savedFavorites);
}, []);
const addToFavorites = () => {
const updatedFavorites = [...favorites, recipe];
setFavorites(updatedFavorites);
localStorage.setItem("favorites", JSON.stringify(updatedFavorites));
};
const removeFromFavorites = () => {
const updatedFavorites = favorites.filter(
(fav) => fav.uri !== recipe.uri
);
setFavorites(updatedFavorites);
localStorage.setItem("favorites", JSON.stringify(updatedFavorites));
};
const isFavorite = favorites.some((fav) => fav.uri === recipe?.uri);
if (loading)
return (
<div className="loader-section">
<div className="loader"></div>
</div>
);
if (error) return <div>{error}</div>;
return (
<div className="recipe-detail">
{recipe && (
<>
<div className="recipe-details-text" >
<h1>{recipe.label}</h1>
<h2>Ingredients:</h2>
<ul>
{recipe.ingredientLines.map((ingredient, index) => (
<li key={index}>{ingredient}</li>
))}
</ul>
<h2>Instructions:</h2>
{/* Note: Edamam API doesn't provide instructions directly. You might need to link to the original recipe URL */}
<p>
For detailed instructions, please visit the{" "}
<a href={recipe.url} target="_blank" rel="noopener noreferrer">
Recipe Instruction
</a>
</p>
{isFavorite ? (
<button className="fav-btn" onClick={removeFromFavorites}>Remove from Favorites</button>
) : (
<button className="fav-btn" onClick={addToFavorites}>Add to Favorites</button>
)}
</div>
<div className="recipe-details-img">
<img src={recipe.image} alt={recipe.label} />
</div>
</>
)}
</div>
);
};
export default RecipeDetail;
Contact.js
This page displays Contact page.
import React, { useState } from 'react';
const Contact = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [showPopup, setShowPopup] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
// Prepare the contact details object
const contactDetails = { name, email, message };
// Save contact details to local storage
const savedContacts = JSON.parse(localStorage.getItem('contacts')) || [];
savedContacts.push(contactDetails);
localStorage.setItem('contacts', JSON.stringify(savedContacts));
// Log the form data
console.log('Form submitted:', contactDetails);
// Clear form fields
setName('');
setEmail('');
setMessage('');
// Show popup
setShowPopup(true);
};
const closePopup = () => {
setShowPopup(false);
};
return (
<div className="contact">
<h1>Contact Us</h1>
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-group">
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="message">Message:</label>
<textarea
id="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
required
></textarea>
</div>
<button type="submit">Submit</button>
</form>
{showPopup && (
<div className="popup">
<div className="popup-inner">
<h2>Thank you!</h2>
<p>Your message has been submitted successfully.</p>
<button onClick={closePopup}>Close</button>
</div>
</div>
)}
</div>
);
};
export default Contact;
About.js
This page displays About Page.
import React from 'react';
const About = () => {
return (
<div className="about">
<div className="about-main">
<h1>About Us</h1>
<p>
Welcome to Recipe Finder, your go-to place for discovering delicious recipes from around the world!
</p>
<p>
Our platform allows you to search for recipes based on your ingredients or dietary preferences. Whether you're looking for a quick meal, a healthy option, or a dish to impress your friends, we have something for everyone.
</p>
<p>
We use the Edamam API to provide you with a vast database of recipes. You can easily find new recipes, view detailed instructions, and explore new culinary ideas.
</p>
<p>
<strong>Features:</strong>
<ul>
<li>Search for recipes by ingredient, cuisine, or dietary restriction.</li>
<li>Browse new and trending recipes.</li>
<li>View detailed recipe instructions and ingredient lists.</li>
<li>Save your favorite recipes for quick access.</li>
</ul>
</p>
<p>
Our mission is to make cooking enjoyable and accessible. We believe that everyone should have the tools to cook great meals at home.
</p>
</div>
</div>
);
};
export default About;
Live Demo
You can view the live demo of the project here.
Conclusion
The Recipe Finder Website is a powerful tool for anyone looking to discover new and trending recipes. By leveraging React for the front end and the Edamam API for data, we can provide a seamless user experience. You can further customize this project by adding features such as pagination, user authentication, or even more detailed filtering options.
Feel free to experiment with the project and make it your own!
Credits
- API: Edamam
- Icons: React Icons
Author
Abhishek Gurjar is a dedicated web developer passionate about creating practical and functional web applications. Check out more of his projects on GitHub.