diff --git a/README.md b/README.md index 88cdbbd..db6b2c8 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,50 @@ Then open [http://localhost:5173](http://localhost:5173) in your browser. --- +## 🍽️ Nutrition Estimation (Recipes page) + +This project optionally integrates the Edamam Nutrition Analysis API to estimate nutrition for a recipe's ingredients on demand. + +Setup (native Edamam): + +1. Create a free Edamam account and an app for the Nutrition Analysis API. +2. Copy keys into a local env file based on `env.example`: + +``` +cp env.example .env +``` + +3. Fill in values: + +``` +VITE_EDAMAM_APP_ID=your_edamam_app_id +VITE_EDAMAM_APP_KEY=your_edamam_app_key +``` + +How it works: +- `src/services/nutrition.js` posts ingredients to `/edamam/api/nutrition-details` through the dev proxy configured in `vite.config.js`. +- Results are cached in-memory per unique ingredients list. +- On `Recipes` cards, click “More Nutrition Info” to fetch and show calories, carbs, protein, fat, fiber, sugar, sodium. + +Notes: +- Keys are only needed locally. Do not commit real keys. The feature gracefully no-ops if keys are missing. + +Using RapidAPI instead of native keys: +1. Subscribe to the Edamam Nutrition Analysis API on RapidAPI. +2. Copy your RapidAPI key and host, then add to `.env` (see `env.example`): + +``` +VITE_RAPIDAPI_KEY=your_rapidapi_key +VITE_RAPIDAPI_HOST=edamam-edamam-nutrition-analysis.p.rapidapi.com +``` + +How selection works: +- If RapidAPI vars are set, the app calls RapidAPI with `x-rapidapi-key`/`x-rapidapi-host`. +- Else if native Edamam vars are set, it uses the native API via the Vite proxy. +- Else it falls back to a local mock JSON for demo/screenshots. + +--- + ## 🤝 Contributing We welcome contributions of all levels — from design tweaks to feature enhancements! diff --git a/env.example b/env.example new file mode 100644 index 0000000..c275270 --- /dev/null +++ b/env.example @@ -0,0 +1,7 @@ +VITE_EDAMAM_APP_ID=your_edamam_app_id +VITE_EDAMAM_APP_KEY=your_edamam_app_key + +# RapidAPI (Edamam Nutrition Analysis via RapidAPI) +VITE_RAPIDAPI_KEY=your_rapidapi_key +VITE_RAPIDAPI_HOST=edamam-edamam-nutrition-analysis.p.rapidapi.com + diff --git a/public/mock/nutrition-sample.json b/public/mock/nutrition-sample.json new file mode 100644 index 0000000..a5694a4 --- /dev/null +++ b/public/mock/nutrition-sample.json @@ -0,0 +1,14 @@ +{ + "uri": "http://example", + "calories": 520, + "totalWeight": 450, + "totalNutrients": { + "PROCNT": { "label": "Protein", "quantity": 32.4, "unit": "g" }, + "CHOCDF": { "label": "Carbs", "quantity": 58.2, "unit": "g" }, + "FAT": { "label": "Fat", "quantity": 18.1, "unit": "g" }, + "SUGAR": { "label": "Sugars", "quantity": 12.7, "unit": "g" }, + "NA": { "label": "Sodium", "quantity": 940, "unit": "mg" }, + "FIBTG": { "label": "Fiber", "quantity": 7.6, "unit": "g" } + } +} + diff --git a/src/pages/Recipes.jsx b/src/pages/Recipes.jsx index ee8d5a1..bc0fdea 100644 --- a/src/pages/Recipes.jsx +++ b/src/pages/Recipes.jsx @@ -18,6 +18,7 @@ * - [ ] Extract service + hook (useMealsSearch) */ import { useEffect, useState } from 'react'; +import { estimateNutritionForIngredients, buildIngredientsFromMealDetail } from '../services/nutrition.js'; import Loading from '../components/Loading.jsx'; import ErrorMessage from '../components/ErrorMessage.jsx'; import Card from '../components/Card.jsx'; @@ -28,6 +29,13 @@ export default function Recipes() { const [randomMeal, setRandomMeal] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [open, setOpen] = useState({}); + const [nutrition, setNutrition] = useState({}); + const [nLoading, setNLoading] = useState({}); + const [nError, setNError] = useState({}); + const hasRapid = Boolean(import.meta.env.VITE_RAPIDAPI_KEY && import.meta.env.VITE_RAPIDAPI_HOST); + const hasEdamam = Boolean(import.meta.env.VITE_EDAMAM_APP_ID && import.meta.env.VITE_EDAMAM_APP_KEY); + const hasProvider = hasRapid || hasEdamam; useEffect(() => { search(); }, []); @@ -51,6 +59,34 @@ export default function Recipes() { } catch (e) { setError(e); } finally { setLoading(false); } } + async function fetchMealDetail(id) { + const res = await fetch(`https://www.themealdb.com/api/json/v1/1/lookup.php?i=${id}`); + if (!res.ok) throw new Error('Failed to fetch'); + const json = await res.json(); + return json.meals?.[0] || null; + } + + async function toggleNutrition(mealId) { + setOpen((o) => ({ ...o, [mealId]: !o[mealId] })); + const isOpening = !open[mealId]; + if (!isOpening) return; + if (nutrition[mealId] || nLoading[mealId]) return; + setNError((e) => ({ ...e, [mealId]: null })); + setNLoading((l) => ({ ...l, [mealId]: true })); + try { + const detail = await fetchMealDetail(mealId); + if (!detail) throw new Error('Meal not found'); + const ingr = buildIngredientsFromMealDetail(detail); + if (ingr.length === 0) throw new Error('No ingredients found'); + const est = await estimateNutritionForIngredients(ingr); + setNutrition((n) => ({ ...n, [mealId]: est })); + } catch (e) { + setNError((er) => ({ ...er, [mealId]: e })); + } finally { + setNLoading((l) => ({ ...l, [mealId]: false })); + } + } + return (
| Calories (kcal) | {data.calories?.toLocaleString?.() || 0} |
| Carbs (g) | {data.carbsG} |
| Protein (g) | {data.proteinG} |
| Fat (g) | {data.fatG} |
| Fiber (g) | {data.fiberG} |
| Sugar (g) | {data.sugarG} |
| Sodium (mg) | {data.sodiumMg} |