使用 React 构建食谱查找器网站
介绍
在本博客中,我们将使用 React 构建一个食谱查找网站。该应用程序允许用户搜索他们最喜欢的食谱,查看趋势或新食谱,并保存他们最喜欢的食谱。我们将利用 Edamam API 获取实时食谱数据并将其动态显示在网站上。
项目概况
食谱查找器允许用户:
- 按名称搜索食谱。
- 查看趋势和新添加的食谱。
- 查看各个食谱的详细信息。
- 将食谱添加到收藏夹列表并使用 localStorage 保存数据。
特征
- 搜索功能:用户可以通过输入查询来搜索食谱。
- 热门食谱:显示来自 API 的当前热门食谱。
- 新菜谱:显示来自 API 的最新菜谱。
- 食谱详细信息:显示有关所选食谱的详细信息。
- 收藏夹:允许用户将食谱添加到收藏夹列表,该列表保存在本地。
使用的技术
- React:用于构建用户界面。
- React Router:用于不同页面之间的导航。
- Edamam API:用于获取食谱。
- CSS:用于设计应用程序的样式。
项目结构
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
安装
要在本地运行此项目,请按照以下步骤操作:
- 克隆存储库:
git clone https://github.com/abhishekgurjar-in/recipe-finder.git cd recipe-finder
- 安装依赖项:
npm install
- 启动 React 应用程序:
npm start
从 Edamam 网站获取您的 Edamam API 凭证(API ID 和 API 密钥)。
在进行 API 调用的页面中添加您的 API 凭据,例如 Home.js、Trending.js、NewRecipe.js 和 RecipeDetail.js。
用法
应用程序.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;
主页.js
这是用户可以使用 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
此页面获取并显示趋势食谱。
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;
新菜谱.js
此页面获取新食谱并显示新食谱。
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;
主页.js
此页面获取并显示主页和搜索的食谱。
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;
收藏夹.js
此页面显示最喜欢的食谱。
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
此页面显示食谱。
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;
联系方式.js
此页面显示联系页面。
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;
关于.js
此页面显示关于页面。
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;
现场演示
您可以在这里查看该项目的现场演示。
结论
食谱查找网站对于任何想要发现新的和流行食谱的人来说是一个强大的工具。通过利用 React 作为前端和 Edamam API 来处理数据,我们可以提供无缝的用户体验。您可以通过添加分页、用户身份验证甚至更详细的过滤选项等功能来进一步自定义此项目。
随意尝试该项目并使其成为您自己的!
制作人员
- API:毛豆
- 图标:React 图标
作者
Abhishek Gurjar 是一位专注的 Web 开发人员,热衷于创建实用且功能性的 Web 应用程序。在 GitHub 上查看他的更多项目。
以上是使用 React 构建食谱查找器网站的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undress AI Tool
免费脱衣服图片

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

Autoprefixer是一个根据目标浏览器范围自动为CSS属性添加厂商前缀的工具。1.它解决了手动维护前缀易出错的问题;2.通过PostCSS插件形式工作,解析CSS、分析需加前缀的属性、依配置生成代码;3.使用步骤包括安装插件、设置browserslist、在构建流程中启用;4.注意事项有不手动加前缀、保持配置更新、非所有属性都加前缀、建议配合预处理器使用。

TocreatestickyheadersandfooterswithCSS,useposition:stickyforheaderswithtopvalueandz-index,ensuringparentcontainersdon’trestrictit.1.Forstickyheaders:setposition:sticky,top:0,z-index,andbackgroundcolor.2.Forstickyfooters,betteruseposition:fixedwithbot

theconic-Gradient()functionIncsscreatesCircularGradientsThatRotateColorStopSaroundAcentralPoint.1.IsidealForPieCharts,ProgressIndicators,colordichers,colorwheels和decorativeBackgrounds.2.itworksbysbysbysbydefindefingincolordefingincolorstopsatspecificains off.

创建CSS加载旋转器的方法有三种:1.使用边框的基本旋转器,通过HTML和CSS实现简单动画;2.使用多个点的自定义旋转器,通过不同延迟时间实现跳动效果;3.在按钮中添加旋转器,通过JavaScript切换类来显示加载状态。每种方法都强调了设计细节如颜色、大小、可访问性和性能优化的重要性,以提升用户体验。

Mobile-firstCSSdesignrequiressettingtheviewportmetatag,usingrelativeunits,stylingfromsmallscreensup,optimizingtypographyandtouchtargets.First,addtocontrolscaling.Second,use%,em,orreminsteadofpixelsforflexiblelayouts.Third,writebasestylesformobile,the

要创建内在响应式网格布局,核心方法是使用CSSGrid的repeat(auto-fit,minmax())模式;1.设置grid-template-columns:repeat(auto-fit,minmax(200px,1fr))让浏览器自动调整列数并限制每列最小和最大宽度;2.使用gap控制格子间距;3.容器应设为相对单位如width:100%、配合box-sizing:border-box避免宽度计算错误并用margin:auto居中;4.可选设置行高与内容对齐方式提升视觉一致性,如row

要让整个网格布局在视口中居中显示,可通过以下方法实现:1.使用margin:0auto实现水平居中,需设定容器固定宽度,适用于固定布局;2.利用Flexbox在外层容器设置justify-content和align-items属性,结合min-height:100vh可实现垂直和水平居中,适合全屏展示场景;3.直接使用CSSGrid的place-items属性在父容器上快速居中,简洁且现代浏览器支持良好,同时需确保父容器有足够高度。每种方式均有适用场景和限制,根据实际需求选择合适的方案即可。

prainuredetectionIncsssusissuse@supportScheckSifabRowsEsuppecifortSupecifortEfeatureBeforeApplyingReplyingStyles.1.itusesconditionalcsssssbasssbasedonproperty-valueperty-valuepairs,suessas@supports@supports@supports@supports(display:grid)
