营养计量器 – 使用React的卡路里追踪应用
GeeksforGeeks营养计量器 应用允许用户输入他们所消耗的食物或菜肴的名称,以及有关蛋白质、卡路里、脂肪、碳水化合物等的详细信息。用户可以跟踪他们的卡路里摄入量,并在超过卡路里限制时收到警告消息。输入项目数据和在卡片格式中保存结果的逻辑已使用JSX实现。该应用使用函数式组件和不同的hooks来管理状态。
最终输出的预览:
先决条件和技术:
- ReactJS
- CSS
- JSX
- 在React中使用函数组件
方法:
为了在ReactJS库中创建营养计量器,我们使用了基于函数的组件,其中我们创建了一个简单的应用程序,它接收用户输入的消耗物品,并接受卡路里、脂肪、碳水化合物等输入。一旦用户输入详细信息,数据将被保存并使用Tailwind CSS以卡片的形式呈现。卡路里的跟踪也被做到了,并对用户可见。默认情况下,卡路里限制为1000卡路里,所以如果用户超过了卡路里限制,会显示警告消息。用户还可以编辑或删除他添加的项目。
创建应用程序的步骤:
步骤1: 在VSCode IDE中使用下面的命令设置React项目。
npx create-react-app nutrition-app
步骤2: 通过执行以下命令导航到新创建的项目文件夹。
cd nutrition-app
步骤3: 由于我们使用Tailwind CSS进行样式设计,我们需要使用npm管理器来安装它。因此,在终端中执行以下命令来安装tailwind CSS。
npm install -D tailwindcss
npx tailwindcss init
步骤4: 执行上述命令后,将生成一个“tailwind.config.js”文件,请将以下内容粘贴到此文件中进行正确配置。
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
步骤5: 使用下面的命令安装所需的依赖项:
npm install --save @fortawesome/react-fontawesome @fortawesome/free-solid-svg-icons font-awesome
步骤6: 现在,在src目录下创建一个名为NutritionMeter.jsx的文件,该文件将包含输入项目细节以及卡路里,脂肪等的完整代码。
项目结构:
在 package.json 中,更新的依赖项将如下所示:
{
"name": "nutrition-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"font-awesome": "^4.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"util": "^0.12.5",
"web-vitals": "^2.1.4"
}
示例: 将以下代码插入到上面目录结构中提到的App.js和NutritionMeter.jsx文件中。
- App.js: 该文件包含NutritionMeter.jsx代码的渲染。所有应用程序都是从此文件开始渲染的。
- NutritionMeter.jsx: 此应用程序中的文件包含所有所需的逻辑,如从用户输入详细信息,计算卡路里,并跟踪卡路里的限制。如果卡路里超过限制,则向用户显示消息。
- NutritionMeter.css: 通过此文件提供支持样式和吸引人的感觉。此文件中指定了所有悬停效果等。
- index.css: 该文件包含用于样式化营养计量器应用程序的Tailwind CSS指令。
//App.js
import React from "react";
import NutritionMeter from "./NutritionMeter";
function App() {
return (
<div className="bg-gray-100 min-h-screen">
<NutritionMeter />
</div>
);
}
export default App;
//NutritionMeter.js File
import React, { useState, useEffect } from "react";
import "./NutritionMeter.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faEdit,
faTrashAlt,
faUtensils,
faPlus,
faMinus,
faTimes,
} from "@fortawesome/free-solid-svg-icons";
const NutritionMeter = () => {
const defaultItemsDisplayed = [
{
id: 1,
name: "Apple",
calories: 52,
protein: 0.26,
carbs: 14,
fat: 1,
quantity: 1,
},
{
id: 2,
name: "Banana",
calories: 89,
protein: 1.09,
carbs: 23,
fat: 5,
quantity: 1,
},
{
id: 3,
name: "Grapes",
calories: 40,
protein: 0.2,
carbs: 20,
fat: 2,
quantity: 1,
},
{
id: 4,
name: "Orange",
calories: 35,
protein: 0.15,
carbs: 25,
fat: 4,
quantity: 1,
},
];
const [nutritionItems, setNutritionItems] = useState(defaultItemsDisplayed);
const [newItem, setNewItem] = useState({
name: "",
calories: "",
protein: "",
carbs: "",
fat: "",
});
const [editItem, setEditItem] = useState(null);
const [totalCalories, setTotalCalories] = useState(0);
const [showWarning, setShowWarning] = useState(false);
const [inputError, setInputError] = useState(false);
useEffect(() => {
const calculateTotalCalories = nutritionItems.reduce(
(total, item) => total + parseFloat(item.calories) * item.quantity,
0
);
setTotalCalories(calculateTotalCalories);
if (calculateTotalCalories > 1000) {
setShowWarning(true);
} else {
setShowWarning(false);
}
}, [nutritionItems]);
const addNutritionItem = () => {
if (
newItem.name &&
newItem.calories >= 0 &&
newItem.protein >= 0 &&
newItem.carbs >= 0 &&
newItem.fat >= 0
) {
setNutritionItems([
...nutritionItems,
{ ...newItem, id: Date.now(), quantity: 1 },
]);
setNewItem({
name: "",
calories: "",
protein: "",
carbs: "",
fat: "",
});
setInputError(false);
} else {
setInputError(true);
}
};
const removeAllItems = () => {
setNutritionItems([]);
};
const editItemFunction = (item) => {
setEditItem(item.id);
setNewItem({ ...item });
};
const updateItemFunction = () => {
if (
newItem.name &&
newItem.calories >= 0 &&
newItem.protein >= 0 &&
newItem.carbs >= 0 &&
newItem.fat >= 0
) {
const updatedItems = nutritionItems.map((item) =>
item.id === newItem.id ? newItem : item
);
setNutritionItems(updatedItems);
setNewItem({
name: "",
calories: "",
protein: "",
carbs: "",
fat: "",
});
setEditItem(null);
setInputError(false);
} else {
setInputError(true);
}
};
const deleteItemFunction = (id) => {
const updatedItems = nutritionItems.filter((item) => item.id !== id);
setNutritionItems(updatedItems);
};
const inputErrorStyle = {
borderColor: "red",
};
const updateItemQuantity = (id, change) => {
const updatedItems = nutritionItems.map((item) =>
item.id === id ? { ...item, quantity: Math.max(item.quantity + change, 1) } : item
);
setNutritionItems(updatedItems);
};
const totalProtein = () => {
return nutritionItems.reduce(
(total, item) => total + parseFloat(item.protein) * item.quantity,
0
);
};
const totalCarbs = () => {
return nutritionItems.reduce(
(total, item) => total + parseFloat(item.carbs) * item.quantity,
0
);
};
const totalFat = () => {
return nutritionItems.reduce(
(total, item) => total + parseFloat(item.fat) * item.quantity,
0
);
};
return (
<div className="bg-green-200 min-h-screen">
<div className="container mx-auto p-4">
<h1 className="text-3xl font-semibold text-center mb-4">
GeeksforGeeks Nutrition Meter
</h1>
{showWarning && (
<div className="bg-red-500 text-white p-2 rounded-md text-center mb-4">
<FontAwesomeIcon icon={faTimes} className="mr-2" />
Total calories exceed recommended limit (1000 calories)!
</div>
)}
<div className="mb-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<input
type="text"
placeholder="Item Name"
className={`w-full py-2 px-3 border rounded-md focus:outline-none
focus:ring focus:border-blue-300 {
inputError && !newItem.name ? "border-red-500" : ""
}`}
style={inputError && !newItem.name ? inputErrorStyle : {}}
value={newItem.name}
onChange={(e) =>
setNewItem({ ...newItem, name: e.target.value })
}
/>
</div>
<div>
<input
type="number"
placeholder="Calories"
className={`w-full py-2 px-3 border rounded-md
focus:outline-none focus:ring focus:border-blue-300{
inputError && newItem.calories < 0 ? "border-red-500" : ""
}`}
style={inputError && newItem.calories < 0 ? inputErrorStyle : {}}
value={newItem.calories}
onChange={(e) =>
setNewItem({ ...newItem, calories: e.target.value })
}
/>
</div>
<div>
<input
type="number"
placeholder="Protein (g)"
className={`w-full py-2 px-3 border rounded-md focus:outline-none
focus:ring focus:border-blue-300 {
inputError && newItem.protein<0 ? "border-red-500" : ""
}`}
style={inputError && newItem.protein<0 ? inputErrorStyle : {}}
value={newItem.protein}
onChange={(e) =>
setNewItem({ ...newItem, protein: e.target.value })
}
/>
</div>
<div>
<input
type="number"
placeholder="Carbs (g)"
className={`w-full py-2 px-3 border rounded-md focus:outline-none
focus:ring focus:border-blue-300{
inputError && newItem.carbs < 0 ? "border-red-500" : ""
}`}
style={inputError && newItem.carbs < 0 ? inputErrorStyle : {}}
value={newItem.carbs}
onChange={(e) =>
setNewItem({ ...newItem, carbs: e.target.value })
}
/>
</div>
<div>
<input
type="number"
placeholder="Fat (g)"
className={`w-full py-2 px-3 border rounded-md focus:outline-none
focus:ring focus:border-blue-300 ${
inputError && newItem.fat < 0 ? "border-red-500" : ""
}`}
style={inputError && newItem.fat < 0 ? inputErrorStyle : {}}
value={newItem.fat}
onChange={(e) =>
setNewItem({ ...newItem, fat: e.target.value })
}
/>
</div>
<div className="col-span-2 sm:col-span-1"></div>
</div>
<div className="mt-3 flex justify-between">
{editItem ? (
<button
className="bg-blue-500 text-white p-3 rounded-md hover:bg-blue-600 mb-4
font-semibold focus:outline-none text-xs"
onClick={updateItemFunction}
>
Update Item
</button>
) : (
<button
className="bg-green-600 text-white p-3 rounded-md hover:bg-green-700 mb-4
font-semibold focus:outline-none text-xs"
onClick={addNutritionItem}
>
Add Item
</button>
)}
<button
className="bg-red-600 text-white p-3 rounded-md font-semibold mb-4 hover:bg-red-700
focus:outline-none text-xs"
onClick={removeAllItems}
>
Clear All
</button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{nutritionItems.map((item) => (
<div
key={item.id}
className="bg-white p-4 rounded-md shadow-md border-2 border-blue-400
hover:border-blue-500 hover:shadow-lg transition transform hover:scale-105"
>
<h2 className="text-lg font-semibold text-gray-800">{item.name}</h2>
<ul className="mt-3">
<li>Calories: {item.calories * item.quantity}</li>
<li>Protein: {item.protein * item.quantity}g</li>
<li>Carbs: {item.carbs * item.quantity}g</li>
<li>Fat: {item.fat * item.quantity}g</li>
<li className="flex items-center mt-2">
<button
className="bg-green-500 text-white hover:bg-green-600 p-2 rounded-md font-semibol"
onClick={() => updateItemQuantity(item.id, 1)}
>
<FontAwesomeIcon icon={faPlus} />
</button>
<span className="mx-2">{item.quantity}</span>
<button
className="bg-red-500 text-white hover:bg-red-600 p-2 rounded-md font-semibol"
onClick={() => updateItemQuantity(item.id, -1)}
>
<FontAwesomeIcon icon={faMinus} />
</button>
</li>
</ul>
<div className="mt-3 flex justify-between">
<button
className="bg-blue-500 text-white pd-2 rounded-md hover:bg-blue-600
font-semibold focus:outline-none text-xs"
onClick={() => editItemFunction(item)}
>
<FontAwesomeIcon icon={faEdit} /> Edit
</button>
<button
className="bg-red-500 text-white pd-2 rounded-md hover:bg-red-600
font-semibold focus:outline-none text-xs"
onClick={() => deleteItemFunction(item.id)}
>
<FontAwesomeIcon icon={faTrashAlt} /> Delete
</button>
</div>
</div>
))}
</div>
<div className="mt-8 text-center">
<p className="text-xl font-semibold">
Total Calories: {totalCalories}{" "}
<span id="nutrition-icon">
<FontAwesomeIcon icon={faUtensils} size="lg" />
</span>
</p>
<p className="text-xl font-semibold">
Total Protein: {totalProtein()}g
</p>
<p className="text-xl font-semibold">
Total Carbs: {totalCarbs()}g
</p>
<p className="text-xl font-semibold">Total Fat: {totalFat()}g</p>
</div>
</div>
</div>
);
};
export default NutritionMeter;
CSS
/*NutritionMeter.css File*/
body {
font-family: Arial, sans-serif;
background-color: #f3f3f3;
margin: 0;
padding: 0;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s, transform 0.3s;
}
.text-center {
text-align: center;
}
.warning {
background-color: #ff6b6b;
color: white;
padding: 10px;
display: flex;
align-items: center;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s;
}
.warning svg {
margin-right: 10px;
}
.clear-button {
background-color: #e74c3c;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 20px;
display: block;
width: 100%;
text-align: center;
font-weight: bold;
transition: background-color 0.3s;
}
.clear-button:hover {
background-color: #c0392b;
}
#nutrition-icon {
color: #3498db;
margin-left: 10px;
}
button {
cursor: pointer;
width: 48%;
padding: 12px 0;
background-color: #3498db;
color: white;
border: none;
border-radius: 8px;
transition: background-color 0.3s;
font-weight: bold;
}
button.edit-button {
margin-right:5%;
}
button.delete-button {
margin-left: 5%;
}
button:hover {
background-color: #2980b9;
}
.error-message {
color: #e74c3c;
font-size: 14px;
margin-top: 5px;
}
@keyframes float {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
100% {
transform: translateY(0);
}
}
.gradient-background {
background: linear-gradient(
to right,
#3498db,
#6dd5fa
);
}
CSS
/* index.css File */
@tailwind base;
@tailwind components;
@tailwind utilities;
运行该应用程序的步骤:
步骤1 。在终端中执行以下命令来运行应用程序。
npm start
步骤2 . 打开像Chrome或Firefox这样的网络浏览器,并在地址栏中输入以下URL。
http://localhost:3000/
输出: