diff --git a/src/components/Filters/index.jsx b/src/components/Filters/index.jsx index f49132e..d09fbcd 100644 --- a/src/components/Filters/index.jsx +++ b/src/components/Filters/index.jsx @@ -1,33 +1,33 @@ /* eslint-disable react/prop-types */ export const RecipeFilters = ({ filters, onFilterChange }) => ( -
- onFilterChange("diet", e?.target?.value)} + className="filter-select" + > + + + + + - - - - - - - - - onFilterChange('maxTime', e?.target?.value)} - className="filter-input" - /> -
+ + onFilterChange("maxTime", e?.target?.value)} + className="filter-input" + /> + ); diff --git a/src/components/RecipeCard/index.jsx b/src/components/RecipeCard/index.jsx index 78ea376..63f87d2 100644 --- a/src/components/RecipeCard/index.jsx +++ b/src/components/RecipeCard/index.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/prop-types */ -import { Clock, Users } from 'lucide-react'; +import { Clock, Users } from "lucide-react"; export const RecipeDetailsCard = ({ recipe, onClick }) => (
onClick(recipe)}> @@ -7,19 +7,16 @@ export const RecipeDetailsCard = ({ recipe, onClick }) => (

{recipe?.title}

-
- - {recipe?.readyInMinutes || '30'} mins +
+ + {recipe?.readyInMinutes || "N/A"} mins
-
- - {recipe?.servings || '4'} servings +
+ + {recipe?.servings || (recipe?.readyInMinutes > 30 ? "6" : "2")}{" "} + servings
- {console.log(recipe?.servings)} - - -
-); \ No newline at end of file +); diff --git a/src/components/RecipeDetails/index.jsx b/src/components/RecipeDetails/index.jsx index c18cd3e..37f345b 100644 --- a/src/components/RecipeDetails/index.jsx +++ b/src/components/RecipeDetails/index.jsx @@ -1,38 +1,36 @@ +/* eslint-disable react/no-unknown-property */ /* eslint-disable react/prop-types */ -import { Clock, Users } from 'lucide-react'; -import { useRecipeNutrition } from '../../hooks/useCalorieUnits'; -import { closeIcon } from '../../svgIcons/svgIcons'; +import { Clock, Users, BookOpen } from "lucide-react"; +import { closeIcon } from "../../svgIcons/svgIcons"; export const RecipeDetails = ({ recipe, onClose }) => { - const { data: nutrition, isLoading } = useRecipeNutrition(recipe?.id); - - return ( -
-
- -

{recipe?.title}

- {recipe?.title} -
-
- - {recipe?.readyInMinutes} mins -
-
- - {recipe?.servings} servings -
- {isLoading ? ( -
Loading nutrition info...
- ) : ( - nutrition && ( -
- Amount: {nutrition?.amount} - Units: {nutrition?.units} -
- ) - )} -
-
+ return ( +
+
+ +

{recipe?.title}

+ {recipe?.title} +
+
+ + {recipe?.readyInMinutes || "N/A"} mins +
+
+ + {recipe?.servings || (recipe?.readyInMinutes > 30 ? "6" : "2")}{" "} + servings +
+
+ + Directions + +
+ {recipe?.instructions || "N/A"} +
- ); +
+
+ ); }; diff --git a/src/components/RecipeFinder.css b/src/components/RecipeFinder.css index db7f358..def036f 100644 --- a/src/components/RecipeFinder.css +++ b/src/components/RecipeFinder.css @@ -1,170 +1,160 @@ .container { - max-width: 1200px; - margin: 0 auto; - padding: 2rem 1rem; - } - - .title { - font-size: 2rem; - font-weight: bold; - text-align: center; - margin-bottom: 2rem; - } - - .search-container { - position: relative; - width: 100%; - max-width: 200px; - margin: 0 auto 1rem; - } - - .search-input { - width: 100%; - padding: 1rem; - padding-left: 3rem; - border: 1px solid #ccc; - border-radius: 8px; - font-size: 1rem; - } - - .search-icon { - position: absolute; - left: 1rem; - top: 50%; - transform: translateY(-50%); - color: #666; - } - - .filters-container { - display: flex; - flex-wrap: wrap; - gap: 1rem; - margin-bottom: 2rem; - } - - .filter-select, - .filter-input { - padding: 1rem; - border: 1px solid #ccc; - border-radius: 4px; - font-size: 1rem; - } - - .recipe-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 1.5rem; - } - - .recipe-card { - background: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - overflow: hidden; - cursor: pointer; - transition: transform 0.2s; - } - - .recipe-card:hover { - transform: scale(1.03); - } - - .recipe-image { - width: 100%; - height: 200px; - object-fit: cover; - } - - .recipe-content { - padding: 1rem; - } - - .recipe-title { - font-size: 1.25rem; - font-weight: 600; - margin-bottom: 0.5rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .recipe-info { - display: flex; - gap: 1rem; - font-size: 0.875rem; - color: #666; - } - - .info-item { - display: flex; - align-items: center; - } - - .info-icon { - margin-right: 0.25rem; - } - - .modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; - } - - .modal-content { - background: white; - border-radius: 8px; - max-width: 600px; - width: 100%; - max-height: 90vh; - overflow-y: auto; - padding: 1.5rem; - position: relative; - } - - .close-button { - position: absolute; - right: 1rem; - top: 1rem; - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: #666; - } - - .close-button:hover { - color: #333; - } - - .modal-title { - font-size: 1.5rem; - font-weight: bold; - margin-bottom: 1rem; - } - - .modal-image { - width: 100%; - height: 300px; - object-fit: cover; - border-radius: 8px; - margin-bottom: 1rem; - } - - .loading { - text-align: center; - padding: 2rem; - } - - .error { - color: #dc2626; - text-align: center; - padding: 2rem; - } + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; +} - .filters-section{ - align-items: center; - } \ No newline at end of file +.title { + font-size: 2rem; + font-weight: bold; + text-align: center; + margin-bottom: 2rem; +} + +.search-container { + position: relative; + max-width: 200px; + margin: 0 auto 1rem; +} + +.search-input { + width: 100%; + padding: 1rem 1rem 1rem 3rem; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 1rem; +} + +.search-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: #666; +} + +.filters-container { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + margin-bottom: 2rem; +} + +.filter-select, +.filter-input { + padding: 1rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; +} + +.recipe-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.recipe-card { + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; + cursor: pointer; + transition: transform 0.2s; +} + +.recipe-card:hover { + transform: scale(1.03); +} + +.recipe-image { + width: 100%; + height: 200px; + object-fit: cover; +} + +.recipe-content { + padding: 1rem; +} + +.recipe-title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 0.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.recipe-info { + display: flex; + gap: 1rem; + font-size: 0.875rem; + color: #666; +} + +.info-item { + display: flex; + align-items: center; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.modal-content { + background: white; + border-radius: 8px; + max-width: 600px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + padding: 1.5rem; + position: relative; +} + +.close-button { + position: absolute; + right: 1rem; + top: 1rem; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #666; +} + +.close-button:hover { + color: #333; +} + +.modal-title { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1rem; +} + +.modal-image { + width: 100%; + height: 300px; + object-fit: cover; + border-radius: 8px; + margin-bottom: 1rem; +} + +.loading, +.error { + text-align: center; + padding: 2rem; +} + +.error { + color: #dc2626; +} diff --git a/src/components/RecipeFinder.jsx b/src/components/RecipeFinder.jsx index 9c4fe4f..dd86d52 100644 --- a/src/components/RecipeFinder.jsx +++ b/src/components/RecipeFinder.jsx @@ -1,55 +1,59 @@ -import { useReducer } from 'react'; -import { RecipeSearchBar } from '../components/SearchBar/index'; -import { RecipeFilters } from '../components/Filters/index'; -import { RecipeGrid } from '../components/RecipeGrid/index'; -import { RecipeDetails } from '../components/RecipeDetails/index'; -import { useRecipes } from '../hooks/useRecipe'; -import { initialState, recipeReducer } from '../store/RecipeStore'; -import '../components/RecipeFinder.css'; +import { useReducer } from "react"; +import { RecipeSearchBar } from "../components/SearchBar/index"; +import { RecipeFilters } from "../components/Filters/index"; +import { RecipeGrid } from "../components/RecipeGrid/index"; +import { RecipeDetails } from "../components/RecipeDetails/index"; +import { useDetailedRecipes } from "../hooks/useRecipe"; +import { initialState, recipeReducer } from "../store/RecipeStore"; +import "../components/RecipeFinder.css"; export const RecipeFinder = () => { - const [state, dispatch] = useReducer(recipeReducer, initialState); - const { data: recipes, isLoading, error } = useRecipes( - state?.searchQuery, - state?.filters - ); + const [state, dispatch] = useReducer(recipeReducer, initialState); + const { + data: recipes, + isLoading, + error, + } = useDetailedRecipes(state?.searchQuery, state?.filters); - const handleSearch = (query) => { - dispatch({ type: 'SET_SEARCH', payload: query }); - }; + const handleSearch = (query) => { + dispatch({ type: "SET_SEARCH", payload: query }); + }; - const handleFilterChange = (field, value) => { - dispatch({ type: 'SET_FILTER', field, payload: value }); - }; + const handleFilterChange = (field, value) => { + dispatch({ type: "SET_FILTER", field, payload: value }); + }; - const handleRecipeClick = (recipe) => { - console.log("Recipe clicked:", recipe); - dispatch({ type: 'SET_SELECTED_RECIPE', payload: recipe }); - }; - - if (error) return
Error loading recipes
+ const handleRecipeClick = (recipe) => { + dispatch({ type: "SET_SELECTED_RECIPE", payload: recipe }); + }; + if (error) return
Error loading recipes
; return (
-

Assignment 4

-

Recipe Finder

-
- - -
- {isLoading ? ( -
Loading recipes...
- ):( - - )} +

Assignment 4

+

Recipe Finder

+
+ + +
+ {isLoading ? ( +
Loading recipes...
+ ) : ( + + )} - {state?.selectedRecipe && ( - dispatch({ type: 'SET_SELECTED_RECIPE', payload: null })} - /> - )} + {state?.selectedRecipe && ( + + dispatch({ type: "SET_SELECTED_RECIPE", payload: null }) + } + /> + )}
); -}; \ No newline at end of file +}; diff --git a/src/components/RecipeGrid/index.jsx b/src/components/RecipeGrid/index.jsx index f66cdb4..97e5ce3 100644 --- a/src/components/RecipeGrid/index.jsx +++ b/src/components/RecipeGrid/index.jsx @@ -1,14 +1,14 @@ /* eslint-disable react/prop-types */ -import { RecipeDetailsCard } from '../RecipeCard/index'; +import { RecipeDetailsCard } from "../RecipeCard/index"; export const RecipeGrid = ({ recipes, onRecipeClick }) => ( -
- {recipes?.map((recipe) => ( - onRecipeClick(recipe)} - /> - ))} -
-); \ No newline at end of file +
+ {recipes?.map((recipe) => ( + onRecipeClick(recipe)} + /> + ))} +
+); diff --git a/src/components/SearchBar/index.jsx b/src/components/SearchBar/index.jsx index 6739e08..b66ba39 100644 --- a/src/components/SearchBar/index.jsx +++ b/src/components/SearchBar/index.jsx @@ -1,14 +1,14 @@ /* eslint-disable react/prop-types */ -import { Search } from 'lucide-react'; +import { Search } from "lucide-react"; export const RecipeSearchBar = ({ onSearch }) => (
onSearch(e?.target?.value)} + onChange={(e) => onSearch(e?.target?.value)} className="search-input" />
-); \ No newline at end of file +); diff --git a/src/components/apiUtils/allApi.js b/src/components/apiUtils/allApi.js new file mode 100644 index 0000000..bc24f76 --- /dev/null +++ b/src/components/apiUtils/allApi.js @@ -0,0 +1,9 @@ +export const fetchRecipeDetails = async (id) => { + const response = await fetch( + `https://api.spoonacular.com/recipes/${id}/information?includeNutrition=false&apiKey=2bed23978b4d417081acee70da2f9f5f` + ); + if (!response?.ok) { + throw new Error("Failed to fetch recipe details"); + } + return await response?.json(); +}; diff --git a/src/hooks/useCalorieUnits.js b/src/hooks/useCalorieUnits.js deleted file mode 100644 index 85a9aea..0000000 --- a/src/hooks/useCalorieUnits.js +++ /dev/null @@ -1,17 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -export const useRecipeNutrition = (recipeId) => { - return useQuery({ - queryKey: ['recipeNutrition', recipeId], - queryFn: async () => { - const response = await fetch( - `https://api.spoonacular.com/recipes/${recipeId}/nutritionWidget.json?apiKey=a2f1ea26b02d4919b35c7152b5ebac6d` - ); - if (!response?.ok) { - throw new Error("Failed to fetch nutrition data"); - } - return response?.json(); - }, - enabled: !!recipeId, - }); -}; diff --git a/src/hooks/useRecipe.js b/src/hooks/useRecipe.js index b01915e..ad729d6 100644 --- a/src/hooks/useRecipe.js +++ b/src/hooks/useRecipe.js @@ -1,23 +1,30 @@ -import { useQuery } from '@tanstack/react-query'; +/* eslint-disable no-unsafe-optional-chaining */ +import { useQuery } from "@tanstack/react-query"; +import { fetchRecipeDetails } from "../components/apiUtils/allApi"; -export const useRecipes = (searchQuery, filters) => { - return useQuery({ - queryKey: ['recipes', searchQuery, filters], - queryFn: async () => { - const params = new URLSearchParams({ - apiKey: 'a2f1ea26b02d4919b35c7152b5ebac6d', - query: searchQuery, - cuisine: filters?.cuisine, - ...(filters?.diet && { diet: filters?.diet }), - ...(filters?.maxTime && { maxReadyTime: filters?.maxTime }), - }); +export const useDetailedRecipes = (searchQuery, filters) => { + return useQuery({ + queryKey: ["detailedRecipes", searchQuery, filters], + queryFn: async () => { + const params = new URLSearchParams({ + apiKey: "2bed23978b4d417081acee70da2f9f5f", + query: searchQuery, + cuisine: filters?.cuisine, + ...(filters?.diet && { diet: filters?.diet }), + ...(filters?.maxTime && { maxReadyTime: filters?.maxTime }), + }); - const response = await fetch( - `https://api.spoonacular.com/recipes/complexSearch?${params}&_start=0&_limit=100` - ); - const data = await response?.json(); - return data?.results; - }, - enabled: true, - }); + const response = await fetch( + `https://api.spoonacular.com/recipes/complexSearch?${params}` + ); + const { results } = await response?.json(); + + const detailedRecipes = await Promise?.all( + results?.map((recipe) => fetchRecipeDetails(recipe?.id)) + ); + + return detailedRecipes; + }, + enabled: true, + }); }; \ No newline at end of file