From 9e99cb0c0a1ce40b900b9835895f1db0fde39db8 Mon Sep 17 00:00:00 2001 From: Farhaan Date: Sat, 22 Aug 2026 14:40:40 +0530 Subject: [PATCH 1/2] feat(pantry-alchemy): add ingredient search and recipe discovery UI --- src/plays/pantry-alchemy/IngredientSearch.js | 248 ++++++++++++++++ src/plays/pantry-alchemy/PantryAlchemy.js | 70 +++++ src/plays/pantry-alchemy/Readme.md | 36 +++ src/plays/pantry-alchemy/RecipeSlider.js | 285 +++++++++++++++++++ src/plays/pantry-alchemy/styles.css | 1 + 5 files changed, 640 insertions(+) create mode 100644 src/plays/pantry-alchemy/IngredientSearch.js create mode 100644 src/plays/pantry-alchemy/PantryAlchemy.js create mode 100644 src/plays/pantry-alchemy/Readme.md create mode 100644 src/plays/pantry-alchemy/RecipeSlider.js create mode 100644 src/plays/pantry-alchemy/styles.css diff --git a/src/plays/pantry-alchemy/IngredientSearch.js b/src/plays/pantry-alchemy/IngredientSearch.js new file mode 100644 index 0000000000..68e256116e --- /dev/null +++ b/src/plays/pantry-alchemy/IngredientSearch.js @@ -0,0 +1,248 @@ +import { useEffect, useState } from 'react'; +import { Autocomplete, TextField, Chip, CircularProgress, Box, Stack, Button } from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; +import RecipeSlider from './RecipeSlider'; + +export default function IngredientAutocomplete({ onSubmit }) { + const [options, setOptions] = useState([]); + const [value, setValue] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [loading, setLoading] = useState(false); + const [recipeLoading, setRecipeLoading] = useState(false); + const [recipes, setRecipes] = useState([]); + const appId = '65a07d9b'; + const appKey = '614cce709d8bbd26d92c35013e5d7861'; + + async function searchRecipe(ingredientsArray) { + // Join array of ingredients: ['chicken', 'garlic', 'spinach'] -> "chicken garlic spinach" + const searchQuery = ingredientsArray + .map((item) => (item.includes(' ') ? `"${item}"` : item)) + .join(' '); + + const params = new URLSearchParams({ + type: 'public', + q: searchQuery, + app_id: appId, + app_key: appKey + }); + + const url = `https://api.edamam.com/api/recipes/v2?${params.toString()}`; + + try { + setRecipeLoading(true); + const response = await fetch(url); + const data = await response.json(); + setRecipes(data.hits); + } catch (error) { + console.error('Error:', error); + } finally { + setRecipeLoading(false); + } + } + + async function searchIngredients(query) { + if (!query.trim()) return []; + + const url = + `https://world.openfoodfacts.org/api/v3/taxonomy_suggestions` + + `?tagtype=ingredients` + + `&lc=en` + + `&string=${encodeURIComponent(query)}` + + `&limit=10`; + + const response = await fetch(url); + const data = await response.json(); + + return data.suggestions || []; + } + useEffect(() => { + const query = inputValue.trim(); + + // Don't search for very short queries + if (query.length < 2) { + setOptions([]); + + return; + } + + const timeout = setTimeout(async () => { + try { + setLoading(true); + + const data = await searchIngredients(query); + + setOptions(data.map((item) => ({ id: item, label: item }))); + } catch (error) { + console.error('Failed to search ingredients:', error); + setOptions([]); + } finally { + setLoading(false); + } + }, 400); // debounce + + return () => clearTimeout(timeout); + }, [inputValue]); + + return ( + + + + option.label || ''} + inputValue={inputValue} + isOptionEqualToValue={(option, value) => option.id === value.id} + loading={loading} + loadingText="Searching..." + noOptionsText={inputValue.length < 2 ? 'Search Ingredients' : 'No ingredients found'} + options={options} + renderInput={(params) => ( + + {loading && } + {params.InputProps.endAdornment} + + ) + } + }} + sx={{ + '& .MuiAutocomplete-input': { + border: 'none !important' + }, + '& .MuiOutlinedInput-root': { + minHeight: 56, + borderRadius: '14px', + backgroundColor: '#fff', + border: 'none', + // Remove black/default border + '& fieldset': { + border: '1px solid #E0E0E0' + }, + + '&:hover fieldset': { + border: '1px solid #BDBDBD' + }, + + '&.Mui-focused fieldset': { + border: '2px solid #4CAF50' + }, + + // Remove black focus outline + '&.Mui-focused': { + outline: 'none', + boxShadow: '0 0 0 3px rgba(76, 175, 80, 0.12)' + } + }, + + '& .MuiInputBase-input': { + outline: 'none !important', + boxShadow: 'none !important' + } + }} + /> + )} + renderTags={(selected, getTagProps) => + selected.map((option, index) => ( + + )) + } + slotProps={{ + paper: { + sx: { + mt: 1, + borderRadius: '14px', + boxShadow: '0 8px 30px rgba(0, 0, 0, 0.10)', + overflow: 'hidden' + } + }, + listbox: { + sx: { + p: '6px', + + '& .MuiAutocomplete-option': { + borderRadius: '9px', + padding: '10px 12px', + marginBottom: '2px', + + '&:hover': { + backgroundColor: '#F1F8F2' + }, + + "&[aria-selected='true']": { + backgroundColor: '#E8F5E9', + color: '#2E7D32' + } + } + } + } + }} + value={value} + onChange={(_, newValue) => { + setValue(newValue); + }} + onInputChange={(_, newInputValue) => { + setInputValue(newInputValue); + }} + /> + + + + + + + + + ); +} diff --git a/src/plays/pantry-alchemy/PantryAlchemy.js b/src/plays/pantry-alchemy/PantryAlchemy.js new file mode 100644 index 0000000000..24b2d78181 --- /dev/null +++ b/src/plays/pantry-alchemy/PantryAlchemy.js @@ -0,0 +1,70 @@ +import PlayHeader from 'common/playlists/PlayHeader'; +import './styles.css'; +import { Stack, Typography } from '@mui/material'; +import IngredientSearch from './IngredientSearch'; + +// WARNING: Do not change the entry componenet name +function PantryAlchemy(props) { + // Example usage: Search for recipes with chicken, garlic, and sweet potato + // searchByIngredients(['chicken', 'garlic', 'sweet potato']); + // Your Code Start below. + + return ( + <> +
+ +
+ {/* Your Code Starts Here */} +
+ + + + Pantry Alchemy + + + + Turn the ingredients in your pantry into something delicious. + + + + + +
+ {/* Your Code Ends Here */} +
+
+ + ); +} + +export default PantryAlchemy; diff --git a/src/plays/pantry-alchemy/Readme.md b/src/plays/pantry-alchemy/Readme.md new file mode 100644 index 0000000000..cd13ab990d --- /dev/null +++ b/src/plays/pantry-alchemy/Readme.md @@ -0,0 +1,36 @@ +# Pantry Alchemy + +An intelligent culinary engine that transforms everyday ingredients into tailored, restaurant-quality dish concepts. + +## Play Demographic + +- Language: js +- Level: Beginner + +## Creator Information + +- User: Farhaan +- Gihub Link: https://github.com/Farhaan +- Blog: +- Video: + +## Implementation Details + +- React 18 application built with **Material UI**, React Hooks, and the browser `fetch` API. +- Ingredient autocomplete uses **Open Food Facts**, with 400ms debounced searches and multi-selection. +- Selected ingredients are submitted to the **Edamam Recipe API** to retrieve matching recipes. +- Recipes are displayed in responsive horizontal cards with images, ingredient summaries, expandable health labels, and source links. + +## Consideration + +- Edamam API credentials are currently hardcoded; move them to environment variables or a backend before production. +- API failures are currently logged to the console rather than displayed to users. +- `sample.json` contains potentially expiring Edamam image URLs and exposed API credentials. +- The application depends on external API availability and browser CORS permissions. + +## Resources + +- **Edamam Recipe API** — recipe search and recipe data. +- **Open Food Facts** — ingredient autocomplete and taxonomy suggestions. +- **Material UI** — UI components, responsive styling, cards, chips, and controls. +- **React** — component architecture, state management, effects, and API interactions. diff --git a/src/plays/pantry-alchemy/RecipeSlider.js b/src/plays/pantry-alchemy/RecipeSlider.js new file mode 100644 index 0000000000..59a8a6a9c8 --- /dev/null +++ b/src/plays/pantry-alchemy/RecipeSlider.js @@ -0,0 +1,285 @@ +import { useState } from 'react'; +import { + Box, + Card, + CardContent, + CardMedia, + Chip, + Collapse, + IconButton, + Typography, + Button, + Stack +} from '@mui/material'; +import { ExpandMore, ArrowForward, FavoriteBorder } from '@mui/icons-material'; + +const chipColors = [ + { bg: '#E8F5E9', color: '#2E7D32' }, + { bg: '#E3F2FD', color: '#1565C0' }, + { bg: '#FFF3E0', color: '#E65100' }, + { bg: '#F3E5F5', color: '#7B1FA2' }, + { bg: '#FCE4EC', color: '#C2185B' } +]; + +const getChipColor = (index) => chipColors[index % chipColors.length]; + +function RecipeCard({ recipe }) { + const [expanded, setExpanded] = useState(false); + + return ( + + {/* Fixed 10rem image */} + + + + + + + + + + {/* Title */} + + {recipe.recipe.label} + + + {/* Ingredient description */} + + Made with {recipe.recipe.ingredientLines.join(', ')}. + + + {/* Health benefits toggle */} + + + {/* Expandable section */} + + + {recipe.recipe.healthLabels.map((label, index) => { + const color = getChipColor(index); + + return ( + + ); + })} + + + + {/* Read more */} + + + + ); +} + +export default function RecipeSlider({ recipes = [] }) { + return ( + + *': { + scrollSnapAlign: 'start' + }, + + scrollbarWidth: 'none', + + '&::-webkit-scrollbar': { + display: 'none' + } + }} + > + {recipes.map((recipe) => ( + + ))} + + + ); +} diff --git a/src/plays/pantry-alchemy/styles.css b/src/plays/pantry-alchemy/styles.css new file mode 100644 index 0000000000..5fd508fa9e --- /dev/null +++ b/src/plays/pantry-alchemy/styles.css @@ -0,0 +1 @@ +/* enter stlyes here */ From 5030799bd617dec6dad4f0175a5676386b299f05 Mon Sep 17 00:00:00 2001 From: Farhaan Date: Sat, 22 Aug 2026 14:44:14 +0530 Subject: [PATCH 2/2] chore(config): move API keys to environment variables --- .env.example | 2 ++ src/plays/pantry-alchemy/IngredientSearch.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 74136cad18..867e372def 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,8 @@ REACT_APP_ACTIVITY_ID=hackrplay REACT_APP_DADJOKES_URL=https://jokeapi-v2.p.rapidapi.com/joke/ REACT_APP_DADJOKES_APIKEY=your_rapidapi_key_here REACT_APP_DADJOKES_APIHOST='jokeapi-v2.p.rapidapi.com' +REACT_APP_APP_EDAMAM_KEY=your_edamam_api_key +REACT_APP_APP_EDAMAM_ID=your_edamam_app_id # Add your API keys below: # REACT_APP_SEARCH_APIKEY=your_search_api_key diff --git a/src/plays/pantry-alchemy/IngredientSearch.js b/src/plays/pantry-alchemy/IngredientSearch.js index 68e256116e..e8f9dd88af 100644 --- a/src/plays/pantry-alchemy/IngredientSearch.js +++ b/src/plays/pantry-alchemy/IngredientSearch.js @@ -10,8 +10,8 @@ export default function IngredientAutocomplete({ onSubmit }) { const [loading, setLoading] = useState(false); const [recipeLoading, setRecipeLoading] = useState(false); const [recipes, setRecipes] = useState([]); - const appId = '65a07d9b'; - const appKey = '614cce709d8bbd26d92c35013e5d7861'; + const appId = process.env.REACT_APP_APP_EDAMAM_ID; + const appKey = process.env.REACT_APP_APP_EDAMAM_KEY; async function searchRecipe(ingredientsArray) { // Join array of ingredients: ['chicken', 'garlic', 'spinach'] -> "chicken garlic spinach"