From 9bbbfa2fad0d0f36af6be717c1336137a43faa8c Mon Sep 17 00:00:00 2001 From: Sahan Tharaka Date: Fri, 17 Oct 2025 21:44:27 +0530 Subject: [PATCH 1/3] Add nutrition estimation feature to Recipes page - Integrated Edamam Nutrition Analysis API for on-demand nutrition estimates. - Updated README with setup instructions for API keys. - Enhanced Recipes component to fetch and display nutrition information for ingredients. - Added loading and error handling for nutrition data retrieval. --- README.md | 30 ++++++++++++++++ env.example | 3 ++ src/pages/Recipes.jsx | 71 +++++++++++++++++++++++++++++++++++--- src/services/nutrition.js | 72 +++++++++++++++++++++++++++++++++++++++ vite.config.js | 5 +++ 5 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 env.example create mode 100644 src/services/nutrition.js diff --git a/README.md b/README.md index 88cdbbd..5f89be2 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,36 @@ 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: + +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. + +--- + ## 🤝 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..2bf959b --- /dev/null +++ b/env.example @@ -0,0 +1,3 @@ +VITE_EDAMAM_APP_ID=your_edamam_app_id +VITE_EDAMAM_APP_KEY=your_edamam_app_key + diff --git a/src/pages/Recipes.jsx b/src/pages/Recipes.jsx index ee8d5a1..0ef1310 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,10 @@ 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({}); useEffect(() => { search(); }, []); @@ -51,6 +56,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 (

Recipe Finder

@@ -68,11 +101,39 @@ export default function Recipes() { )}
- {meals.map(m => ( - - - - ))} + {meals.map(m => { + const isOpen = !!open[m.idMeal]; + const data = nutrition[m.idMeal]; + const isNL = !!nLoading[m.idMeal]; + const err = nError[m.idMeal]; + return ( + + + + {isOpen && ( +
+ {isNL && } + + {data && !isNL && !err && ( + + + + + + + + + + +
Calories{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}
+ )} +
+ )} +
+ ); + })}
); diff --git a/src/services/nutrition.js b/src/services/nutrition.js new file mode 100644 index 0000000..a796a3c --- /dev/null +++ b/src/services/nutrition.js @@ -0,0 +1,72 @@ +const cache = new Map(); + +const getEnv = () => ({ + appId: import.meta.env.VITE_EDAMAM_APP_ID, + appKey: import.meta.env.VITE_EDAMAM_APP_KEY, +}); + +const normalize = (apiResponse) => { + if (!apiResponse) return null; + const n = apiResponse.totalNutrients || {}; + return { + calories: Math.round(apiResponse.calories || 0), + weight: Math.round(apiResponse.totalWeight || 0), + proteinG: Math.round((n.PROCNT?.quantity || 0)), + carbsG: Math.round((n.CHOCDF?.quantity || 0)), + fatG: Math.round((n.FAT?.quantity || 0)), + sugarG: Math.round((n.SUGAR?.quantity || 0)), + sodiumMg: Math.round((n.NA?.quantity || 0)), + fiberG: Math.round((n.FIBTG?.quantity || 0)), + raw: apiResponse, + }; +}; + +const ingredientsKey = (ingredients) => ingredients.map((s) => s.trim().toLowerCase()).sort().join('\n'); + +export async function estimateNutritionForIngredients(ingredients) { + const { appId, appKey } = getEnv(); + if (!Array.isArray(ingredients) || ingredients.length === 0) return null; + + const key = ingredientsKey(ingredients); + if (cache.has(key)) return cache.get(key); + + if (!appId || !appKey) { + return null; + } + + const url = `/edamam/api/nutrition-details?app_id=${encodeURIComponent(appId)}&app_key=${encodeURIComponent(appKey)}`; + + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'Recipe', ingr: ingredients }), + }); + + if (!res.ok) { + throw new Error('Failed to fetch nutrition'); + } + + const json = await res.json(); + const normalized = normalize(json); + cache.set(key, normalized); + return normalized; +} + +export function buildIngredientsFromMealDetail(mealDetail) { + const list = []; + for (let i = 1; i <= 20; i++) { + const ing = mealDetail[`strIngredient${i}`]; + const meas = mealDetail[`strMeasure${i}`]; + if (ing && ing.trim()) { + const line = [meas, ing].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); + list.push(line); + } + } + return list; +} + +export function getCacheSize() { + return cache.size; +} + + diff --git a/vite.config.js b/vite.config.js index 68c1aef..58e5d03 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,6 +9,11 @@ export default defineConfig({ target: 'http://api.open-notify.org', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') + }, + '/edamam': { + target: 'https://api.edamam.com', + changeOrigin: true, + rewrite: (p) => p.replace(/^\/edamam/, '') } } } From 8e1753fe50842b91cc72469c0494605c1271a159 Mon Sep 17 00:00:00 2001 From: Sahan Tharaka Date: Sat, 18 Oct 2025 13:26:40 +0530 Subject: [PATCH 2/3] Add RapidAPI support for Edamam Nutrition Analysis - Introduced RapidAPI integration for nutrition estimation, allowing users to utilize RapidAPI keys. - Updated README with instructions for using RapidAPI instead of native Edamam keys. - Enhanced Recipes component to check for available API providers and adjust UI accordingly. - Implemented fallback mechanisms for nutrition data retrieval, including mock data for demo purposes. --- README.md | 16 +++++++- env.example | 4 ++ public/mock/nutrition-sample.json | 14 +++++++ src/pages/Recipes.jsx | 7 +++- src/services/nutrition.js | 63 +++++++++++++++++++++++++------ 5 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 public/mock/nutrition-sample.json diff --git a/README.md b/README.md index 5f89be2..db6b2c8 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Then open [http://localhost:5173](http://localhost:5173) in your browser. This project optionally integrates the Edamam Nutrition Analysis API to estimate nutrition for a recipe's ingredients on demand. -Setup: +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`: @@ -82,6 +82,20 @@ How it works: 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 diff --git a/env.example b/env.example index 2bf959b..c275270 100644 --- a/env.example +++ b/env.example @@ -1,3 +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 0ef1310..bc0fdea 100644 --- a/src/pages/Recipes.jsx +++ b/src/pages/Recipes.jsx @@ -33,6 +33,9 @@ export default function Recipes() { 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(); }, []); @@ -110,7 +113,7 @@ export default function Recipes() { {isOpen && (
@@ -119,7 +122,7 @@ export default function Recipes() { {data && !isNL && !err && ( - + diff --git a/src/services/nutrition.js b/src/services/nutrition.js index a796a3c..4189fcc 100644 --- a/src/services/nutrition.js +++ b/src/services/nutrition.js @@ -5,6 +5,11 @@ const getEnv = () => ({ appKey: import.meta.env.VITE_EDAMAM_APP_KEY, }); +const getRapidEnv = () => ({ + rapidKey: import.meta.env.VITE_RAPIDAPI_KEY, + rapidHost: import.meta.env.VITE_RAPIDAPI_HOST, +}); + const normalize = (apiResponse) => { if (!apiResponse) return null; const n = apiResponse.totalNutrients || {}; @@ -25,27 +30,61 @@ const ingredientsKey = (ingredients) => ingredients.map((s) => s.trim().toLowerC export async function estimateNutritionForIngredients(ingredients) { const { appId, appKey } = getEnv(); + const { rapidKey, rapidHost } = getRapidEnv(); if (!Array.isArray(ingredients) || ingredients.length === 0) return null; const key = ingredientsKey(ingredients); if (cache.has(key)) return cache.get(key); - if (!appId || !appKey) { - return null; + // Prefer RapidAPI if configured + if (rapidKey && rapidHost) { + const isMulti = ingredients.length > 1; + const nutritionType = isMulti ? 'cooking' : 'logging'; + const ingrString = isMulti ? ingredients.join(', ') : String(ingredients[0]); + const base = `https://${rapidHost}`; + const paths = [ + `/api/nutrition-details?ingr=${encodeURIComponent(ingrString)}&nutrition-type=${nutritionType}`, + `/api/nutrition-data?ingr=${encodeURIComponent(ingrString)}&nutrition-type=${nutritionType}`, + ]; + for (const p of paths) { + const res = await fetch(`${base}${p}`, { + method: 'GET', + headers: { + 'x-rapidapi-key': rapidKey, + 'x-rapidapi-host': rapidHost, + } + }); + if (res.ok) { + const json = await res.json(); + const normalized = normalize(json); + cache.set(key, normalized); + return normalized; + } + if (res.status !== 404) { + throw new Error('Failed to fetch nutrition'); + } + } + throw new Error('Nutrition endpoint not found on RapidAPI host'); } - const url = `/edamam/api/nutrition-details?app_id=${encodeURIComponent(appId)}&app_key=${encodeURIComponent(appKey)}`; - - const res = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title: 'Recipe', ingr: ingredients }), - }); - - if (!res.ok) { - throw new Error('Failed to fetch nutrition'); + // Fallback to native Edamam if configured + if (appId && appKey) { + const url = `/edamam/api/nutrition-details?app_id=${encodeURIComponent(appId)}&app_key=${encodeURIComponent(appKey)}`; + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'Recipe', ingr: ingredients }), + }); + if (!res.ok) throw new Error('Failed to fetch nutrition'); + const json = await res.json(); + const normalized = normalize(json); + cache.set(key, normalized); + return normalized; } + // Final fallback to mock + const res = await fetch('/mock/nutrition-sample.json'); + if (!res.ok) return null; const json = await res.json(); const normalized = normalize(json); cache.set(key, normalized); From 130efa140735ae2de2e1f59aa557f8d06e329b74 Mon Sep 17 00:00:00 2001 From: Sahan Tharaka Date: Sat, 18 Oct 2025 13:43:00 +0530 Subject: [PATCH 3/3] Refactor README for Nutrition Estimation feature - Simplified and clarified setup instructions for nutrition estimation using RapidAPI or native Edamam. - Consolidated usage notes and troubleshooting tips for better user guidance. - Enhanced descriptions of fallback mechanisms for API key handling and mock data usage. --- README.md | 67 +++++++++++++++++++++---------------------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index db6b2c8..a7ef2ca 100644 --- a/README.md +++ b/README.md @@ -54,47 +54,32 @@ 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. +## 🍽️ Nutrition (Recipes) — optional + +Estimate nutrition for a recipe's ingredients. Works with RapidAPI or native Edamam; falls back to a local mock if no keys are set. + +Setup +1. Create `.env` from the example: + - `cp env.example .env` +2. Choose ONE provider: + - RapidAPI (recommended for quick start): + - `VITE_RAPIDAPI_KEY=your_rapidapi_key` + - `VITE_RAPIDAPI_HOST=` + (e.g. `edamam-nutrition-analysis.p.rapidapi.com` — value may vary) + - Native Edamam: + - `VITE_EDAMAM_APP_ID=your_edamam_app_id` + - `VITE_EDAMAM_APP_KEY=your_edamam_app_key` +3. Restart dev server. + +Use +- Open Recipes → click “More Nutrition Info”. +- Shows: Calories (kcal), Carbs (g), Protein (g), Fat (g), Fiber (g), Sugar (g), Sodium (mg). +- If no keys are set, the button shows “Demo Nutrition (mock)”. + +Notes & troubleshooting +- Do not commit real keys. `.env` is local only. +- RapidAPI: ensure Host matches your snippet exactly; we support common endpoints. +- If you see 401/404, double‑check Host/key and quota, then restart the dev server. ---
Calories{data.calories?.toLocaleString?.() || 0}
Calories (kcal){data.calories?.toLocaleString?.() || 0}
Carbs (g){data.carbsG}
Protein (g){data.proteinG}
Fat (g){data.fatG}