├── postcss.config.js ├── tailwind.config.js ├── src ├── components │ ├── NotFound.jsx │ ├── FryingPan.jsx │ ├── Favourites.jsx │ ├── Home.jsx │ ├── Footer.jsx │ ├── Recipe.jsx │ ├── Navbar.jsx │ ├── RecipeItem.jsx │ └── FryingPanStyles.css ├── index.js ├── index.css ├── hooks │ └── useFetch.js └── App.jsx ├── public └── index.html ├── .gitignore ├── package.json └── note.txt /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /src/components/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NotFound = () => { 4 | return ( 5 |
6 |

404 Not Found

7 |
8 | ); 9 | }; 10 | 11 | export default NotFound; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Food Verse Project 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | import './index.css'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/components/FryingPan.jsx: -------------------------------------------------------------------------------- 1 | import "./fryingPanStyles.css"; 2 | 3 | const FryingPan = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ); 22 | }; 23 | 24 | export default FryingPan; -------------------------------------------------------------------------------- /src/components/Favourites.jsx: -------------------------------------------------------------------------------- 1 | import Recipe from "./Recipe"; 2 | 3 | const Favourites = ({ savedItems }) => { 4 | return ( 5 |
6 | {savedItems.length === 0 && ( 7 |

8 | Favourite list is empty! 9 |

10 | )} 11 | 12 |
13 | {savedItems.map((recipe) => ( 14 | 15 | ))} 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Favourites; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url('https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700;800;900&display=swap'); 6 | 7 | body{ 8 | font-family: 'Rubik', sans-serif; 9 | } 10 | 11 | ::selection { 12 | background-color: #0ea5e9; 13 | color: #f0f9ff; 14 | } 15 | 16 | ::-webkit-scrollbar { 17 | width: 10px; 18 | } 19 | 20 | ::-webkit-scrollbar-thumb{ 21 | height: 10%; 22 | background-color: #f43f5e; 23 | border-radius: 50px; 24 | } 25 | 26 | ::-webkit-scrollbar-track{ 27 | background-color: #fff1f2; 28 | } 29 | .site-footer{ 30 | border-radius: 50px 50px 0 0px; 31 | padding: 50px; 32 | } 33 | .text-left p{ 34 | font-size: 22px; 35 | } -------------------------------------------------------------------------------- /src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import Recipe from "./Recipe"; 2 | import FryingPan from "./FryingPan"; 3 | 4 | const Home = ({ recipes, loading, error }) => { 5 | return ( 6 |
7 | {!loading && !error && recipes.length === 0 ? ( 8 |
9 |

10 | Nothing to show, please search something! 11 |

12 | 13 |
14 | ) : null} 15 | 16 | {loading &&

{error ? error : "Loading..."}

} 17 | 18 | {recipes?.length > 0 && 19 | recipes.map((recipe) => )} 20 |
21 | ); 22 | }; 23 | 24 | export default Home; -------------------------------------------------------------------------------- /src/hooks/useFetch.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useFetch = (id) => { 4 | const [data, setData] = useState({}); 5 | const [loading, setLoading] = useState(false); 6 | const [error, setError] = useState(""); 7 | 8 | useEffect(() => { 9 | const getRecipeItemData = async () => { 10 | try { 11 | setLoading(true); 12 | const res = await fetch( 13 | `https://forkify-api.herokuapp.com/api/v2/recipes/${id}` 14 | ); 15 | if (!res.ok) 16 | throw new Error("Something went wrong, please try again later!"); 17 | const data = await res.json(); 18 | setData(data?.data?.recipe); 19 | setLoading(false); 20 | } catch (err) { 21 | setError(err.message); 22 | } 23 | }; 24 | 25 | getRecipeItemData(); 26 | }, []); 27 | 28 | return { data, loading, error }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => { 4 | return ( 5 | 26 | ); 27 | }; 28 | 29 | export default Footer; -------------------------------------------------------------------------------- /src/components/Recipe.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Recipe = ({ recipe }) => { 4 | return ( 5 |
6 | {recipe.title} 11 |
12 | 13 | {recipe.publisher} 14 | 15 |

16 | {recipe.title}{" "} 17 |

18 | 22 | View recipe 23 | 24 |
25 |
26 | ); 27 | }; 28 | 29 | export default Recipe; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "18", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-redux": "^8.0.5", 12 | "react-router-dom": "^6.5.0", 13 | "react-scripts": "5.0.1", 14 | "redux": "^4.2.0", 15 | "web-vitals": "^2.1.4" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "autoprefixer": "^10.4.13", 43 | "postcss": "^8.4.20", 44 | "tailwindcss": "^3.2.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | 3 | const Navbar = ({ 4 | searchHandler, 5 | searchQuery, 6 | setSearchQuery, 7 | inputField, 8 | savedItems, 9 | }) => { 10 | const navActive = ({ isActive }) => { 11 | return { 12 | color: isActive ? "#f43f5e" : null, 13 | }; 14 | }; 15 | 16 | return ( 17 |
18 |

19 | Foodverse 20 |

21 |
22 | setSearchQuery(e.target.value)} 26 | type="search" 27 | placeholder="Search recipe..." 28 | required 29 | className="bg-white/75 p-3 px-8 lg:w-96 rounded-full outline-none shadow-lg shadow-rose-100 focus:shadow-rose-200 duration-300" 30 | /> 31 |
32 | 56 |
57 | ); 58 | }; 59 | 60 | export default Navbar; -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { Route, Routes, useNavigate } from "react-router-dom"; 3 | import Home from "./components/Home"; 4 | import Navbar from "./components/Navbar"; 5 | import Footer from "./components/Footer"; 6 | import Favourites from "./components/Favourites"; 7 | import NotFound from "./components/NotFound"; 8 | import RecipeItem from "./components/RecipeItem"; 9 | 10 | const App = () => { 11 | const [searchQuery, setSearchQuery] = useState(""); 12 | const [recipes, setRecipes] = useState([]); 13 | const [loading, setLoading] = useState(false); 14 | const [error, setError] = useState(""); 15 | const [savedItems, setSavedItems] = useState(() => { 16 | const localData = localStorage.getItem("recipes"); 17 | return localData ? JSON.parse(localData) : []; 18 | }); 19 | 20 | const inputField = useRef(null); 21 | 22 | const navigate = useNavigate(); 23 | 24 | const searchHandler = (e) => { 25 | e.preventDefault(); 26 | 27 | getData(searchQuery); 28 | 29 | setSearchQuery(""); 30 | inputField.current.blur(); 31 | setRecipes([]); 32 | setError(""); 33 | navigate("/"); 34 | }; 35 | 36 | const getData = async (searchQuery) => { 37 | try { 38 | setLoading(true); 39 | const res = await fetch( 40 | `https://forkify-api.herokuapp.com/api/v2/recipes?search=${searchQuery}` 41 | ); 42 | if (!res.ok) throw new Error("Something went wrong!"); 43 | const data = await res.json(); 44 | if (data.results === 0) throw new Error("No recipe found!"); 45 | setRecipes(data?.data?.recipes); 46 | setLoading(false); 47 | } catch (err) { 48 | setError(err.message); 49 | } 50 | }; 51 | 52 | const checkLocalData = (data) => { 53 | const localData = JSON.parse(localStorage.getItem("recipes")); 54 | const existedData = localData?.some((item) => item.id === data.id); 55 | 56 | if (!existedData) { 57 | setSavedItems([...savedItems, data]); 58 | } else { 59 | const filteredData = localData.filter((item) => item.id !== data.id); 60 | setSavedItems(filteredData); 61 | } 62 | }; 63 | 64 | const favouriteHandler = (id) => { 65 | fetch(`https://forkify-api.herokuapp.com/api/v2/recipes/${id}`) 66 | .then((res) => res.json()) 67 | .then((data) => checkLocalData(data.data.recipe)); 68 | 69 | navigate("/favourites"); 70 | }; 71 | 72 | useEffect(() => { 73 | localStorage.setItem("recipes", JSON.stringify(savedItems)); 74 | }, [savedItems]); 75 | 76 | return ( 77 | <> 78 |
79 | 86 | 87 | } 90 | /> 91 | } 94 | /> 95 | 102 | } 103 | /> 104 | } /> 105 | 106 |
108 | 109 | ); 110 | }; 111 | 112 | export default App; -------------------------------------------------------------------------------- /src/components/RecipeItem.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Link, useParams } from "react-router-dom"; 3 | import { useFetch } from "../hooks/useFetch"; 4 | 5 | const RecipeItem = ({ favouriteHandler, savedItems }) => { 6 | const [itemSavedStatus, setItemSavedStatus] = useState(null); 7 | const { id } = useParams(); 8 | 9 | const { data: recipe, loading, error } = useFetch(id); 10 | 11 | const durationCalc = (duration) => { 12 | if (!duration) return; 13 | 14 | if (!String(duration).includes(".")) { 15 | return duration + "h"; 16 | } 17 | 18 | if (String(duration).includes(".")) { 19 | const splittedDuration = String(duration).split("."); 20 | const hour = splittedDuration[0] + "h"; 21 | const splitterMinutes = "." + splittedDuration[1]; 22 | const minutes = +splitterMinutes * 60 + "min"; 23 | 24 | return hour + minutes; 25 | } 26 | }; 27 | 28 | useEffect(() => { 29 | if (!recipe) return; 30 | 31 | setItemSavedStatus(savedItems.some((item) => item.id === recipe.id)); 32 | }, [recipe]); 33 | 34 | return ( 35 |
36 |
37 |
38 | {recipe?.title} 43 |
44 |
45 | 46 | Ingredients: 47 | 48 |
    49 | {recipe?.ingredients?.map((ing, i) => ( 50 |
  • 51 | ✓ {ing.quantity} 52 | {ing.unit} {ing.description} 53 |
  • 54 | ))} 55 |
56 |
57 |
58 |
59 | 60 | {recipe?.publisher} 61 | 62 |

{recipe?.title}

63 |
64 |
Servings: {recipe?.servings} people
65 |
66 | Cooking time:{" "} 67 | {recipe?.cooking_time < 60 68 | ? String(recipe?.cooking_time) + "min" 69 | : durationCalc(recipe?.cooking_time / 60)} 70 |
71 |
72 |
73 | 85 | 91 | Get directions 92 | 93 | 97 | Back to home 98 | 99 |
100 |
101 |
102 | ); 103 | }; 104 | 105 | export default RecipeItem; -------------------------------------------------------------------------------- /src/components/FryingPanStyles.css: -------------------------------------------------------------------------------- 1 | #cooking { 2 | position: relative; 3 | margin: 0 auto; 4 | top: 0; 5 | width: 50vh; 6 | height: 50vh; 7 | overflow: hidden; 8 | } 9 | 10 | #cooking #area { 11 | position: absolute; 12 | bottom: 0; 13 | right: 0; 14 | width: 50%; 15 | height: 50%; 16 | background-color: transparent; 17 | transform-origin: 15% 60%; 18 | animation: flip 2.1s ease-in-out infinite; 19 | } 20 | 21 | #cooking #area #sides { 22 | position: absolute; 23 | width: 100%; 24 | height: 100%; 25 | transform-origin: 15% 60%; 26 | animation: switchSide 2.1s ease-in-out infinite; 27 | } 28 | 29 | #cooking #area #sides #handle { 30 | position: absolute; 31 | bottom: 18%; 32 | right: 80%; 33 | width: 35%; 34 | height: 20%; 35 | background-color: transparent; 36 | border-top: 1vh solid #374151; 37 | border-left: 1vh solid transparent; 38 | border-radius: 100%; 39 | transform: rotate(20deg) rotateX(0deg) scale(1.3, 0.9); 40 | } 41 | 42 | #cooking #area #sides #pan { 43 | position: absolute; 44 | bottom: 20%; 45 | right: 30%; 46 | width: 50%; 47 | height: 8%; 48 | background-color: #374151; 49 | border-radius: 0 0 1.4em 1.4em; 50 | transform-origin: -15% 0; 51 | } 52 | 53 | #cooking #area #pancake { 54 | position: absolute; 55 | top: 24%; 56 | width: 100%; 57 | height: 100%; 58 | transform: rotateX(85deg); 59 | animation: jump 2.1s ease-in-out infinite; 60 | } 61 | 62 | #cooking #area #pancake #pastry { 63 | position: absolute; 64 | bottom: 26%; 65 | right: 37%; 66 | width: 40%; 67 | height: 45%; 68 | background-color: #fca5a5; 69 | box-shadow: 0 0 3px 0 #fca5a5; 70 | border-radius: 100%; 71 | transform-origin: -20% 0; 72 | animation: fly 2.1s ease-in-out infinite; 73 | } 74 | 75 | #cooking .bubble { 76 | position: absolute; 77 | border-radius: 100%; 78 | box-shadow: 0 0 0.25vh #e5e7eb; 79 | opacity: 0; 80 | } 81 | 82 | #cooking .bubble:nth-child(1) { 83 | margin-top: 2.5vh; 84 | left: 58%; 85 | width: 2.5vh; 86 | height: 2.5vh; 87 | background-color: #d1d5db; 88 | animation: bubble 2s cubic-bezier(0.53, 0.16, 0.39, 0.96) infinite; 89 | } 90 | 91 | #cooking .bubble:nth-child(2) { 92 | margin-top: 3vh; 93 | left: 52%; 94 | width: 2vh; 95 | height: 2vh; 96 | background-color: #9ca3af; 97 | animation: bubble 2s ease-in-out 0.35s infinite; 98 | } 99 | 100 | #cooking .bubble:nth-child(3) { 101 | margin-top: 1.8vh; 102 | left: 50%; 103 | width: 1.5vh; 104 | height: 1.5vh; 105 | background-color: #6b7280; 106 | animation: bubble 1.5s cubic-bezier(0.53, 0.16, 0.39, 0.96) 0.55s infinite; 107 | } 108 | 109 | #cooking .bubble:nth-child(4) { 110 | margin-top: 2.7vh; 111 | left: 56%; 112 | width: 1.2vh; 113 | height: 1.2vh; 114 | background-color: #4b5563; 115 | animation: bubble 1.8s cubic-bezier(0.53, 0.16, 0.39, 0.96) 0.55s infinite; 116 | } 117 | 118 | #cooking .bubble:nth-child(5) { 119 | margin-top: 2.7vh; 120 | left: 63%; 121 | width: 1.1vh; 122 | height: 1.1vh; 123 | background-color: #374151; 124 | animation: bubble 1.6s ease-in-out 1s infinite; 125 | } 126 | 127 | @keyframes pulse { 128 | 0% { 129 | transform: scale(1, 1); 130 | opacity: 0.25; 131 | } 132 | 50% { 133 | transform: scale(1.2, 1); 134 | opacity: 1; 135 | } 136 | 100% { 137 | transform: scale(1, 1); 138 | opacity: 0.25; 139 | } 140 | } 141 | 142 | @keyframes flip { 143 | 0% { 144 | transform: rotate(0deg); 145 | } 146 | 5% { 147 | transform: rotate(-27deg); 148 | } 149 | 30%, 150 | 50% { 151 | transform: rotate(0deg); 152 | } 153 | 55% { 154 | transform: rotate(27deg); 155 | } 156 | 83.3% { 157 | transform: rotate(0deg); 158 | } 159 | 100% { 160 | transform: rotate(0deg); 161 | } 162 | } 163 | 164 | @keyframes switchSide { 165 | 0% { 166 | transform: rotateY(0deg); 167 | } 168 | 50% { 169 | transform: rotateY(180deg); 170 | } 171 | 100% { 172 | transform: rotateY(0deg); 173 | } 174 | } 175 | 176 | @keyframes jump { 177 | 0% { 178 | top: 24; 179 | transform: rotateX(85deg); 180 | } 181 | 25% { 182 | top: 10%; 183 | transform: rotateX(0deg); 184 | } 185 | 50% { 186 | top: 30%; 187 | transform: rotateX(85deg); 188 | } 189 | 75% { 190 | transform: rotateX(0deg); 191 | } 192 | 100% { 193 | transform: rotateX(85deg); 194 | } 195 | } 196 | 197 | @keyframes fly { 198 | 0% { 199 | bottom: 26%; 200 | transform: rotate(0deg); 201 | } 202 | 10% { 203 | bottom: 40%; 204 | } 205 | 50% { 206 | bottom: 26%; 207 | transform: rotate(-190deg); 208 | } 209 | 80% { 210 | bottom: 40%; 211 | } 212 | 100% { 213 | bottom: 26%; 214 | transform: rotate(0deg); 215 | } 216 | } 217 | 218 | @keyframes bubble { 219 | 0% { 220 | transform: scale(0.15, 0.15); 221 | top: 80%; 222 | opacity: 0; 223 | } 224 | 50% { 225 | transform: scale(1.1, 1.1); 226 | opacity: 1; 227 | } 228 | 100% { 229 | transform: scale(0.33, 0.33); 230 | top: 60%; 231 | opacity: 0; 232 | } 233 | } 234 | 235 | @media screen and (max-width: 768px) { 236 | #cooking { 237 | display: none; 238 | } 239 | } -------------------------------------------------------------------------------- /note.txt: -------------------------------------------------------------------------------- 1 | food verse website 2 | 3 | 1. first install tailwind css in your project 4 | 2. select Rubik google font family and set up your project 5 | 3. copy the import link of this fonts and past css file and also body font-family 6 | 4. project start korar age css file kichu starter pack starter css likte hobe css file 7 | body(font-family), 8 | ::selection(), 9 | ::-webkit-scrollbar(width), 10 | ::-webkit-scrollbar-thumb(height, background-color, border-radius), 11 | ::-webkit-scrollbar-track(background-color) 12 | 13 | 5. website er jonno kichu componnet create korte hobe 14 | -Home.jsx 15 | -Navbar.jsx 16 | -Favourites.jsx 17 | -Receipe.jsx 18 | -ReceipeItem.jsx 19 | -fryingPen.jsx 20 | -fryingPenStyle.css 21 | -NotFound.jsx 22 | -Footer.jsx 23 | sob gula functional component hobe 24 | 25 | 6. setup router and install it (npm i react-router-dom) 26 | index.js jete hobe and then modde React.StrictMode ta dukate hobe 27 | 28 | 7. app.js e jete hobe and sekhane apnake div er bitor e route set korte hobe 29 | 8. app.js Footer component ke set korte hobe jate footer ta alltime niche e thake content thakok r nai thakok tar jonno div er bahire Footer ta ke set rakte hobe but jokon rakben tokon abar error show korbe tai take fragment diye rakte and then er bitor footer rakte hobe and div er bitore ekta class dite hobe (min-h-screen) tahole footer ekdom niche chole jabe 30 | 31 | //first step done starter pack o bolte paren ato kon ja korano holo 32 | 33 | //lets deign the page with tailwind css 34 | 35 | 9. first Navbar component ke design korte hobe. amader design kora navbar e 3 ta jinish thakbe (logo, searchbar and menu ) 36 | 37 | 10. navbar name ekta class div nite and then oitar bitor logo search menu thakbe 38 | - first h2 diye logo tar name likte hobe r jehot logo ta 2 colors tai h2 er bitor baki color er onghso toko ke span tag er bitor diye dite hobe. 39 | - search-bar name class form nite hobe jate kew search korle submit korte pare . and oitar bitor ekta input nite hobe jar type hobe search and placholder jekono ekta dile hobe apnr design moto 40 | - menu class name ul tag nite hobe er bitor thakbe Li and li er bitor a ke dite hobe jehoto amra router use korchi tai navlink or link use korte pare a er bodli ei doi tai a ke represent kore abar navlink use korle er bitor (en to="/") use korte hobe 41 | 42 | Home 43 |
  • 44 | 45 | Favourites {} 46 | 47 |
  • 48 | 49 | 10. navbar class div jeita okhane flex boshate hobe justify-between , items-center dite hobe (flex justify-between items-center container mx-auto py-10 flex-col lg:flex-row gap-5) 50 | 51 | 11. h2 er logo ke ekon style korte hobe h2 er bitor ja ja dite hobe 52 | (text-2xl font-bold lowercase italic) and span er bitor dite hobe text-rose-500 53 | 54 | 55 | 12. abar app.js e jete hobe and app class e abr kichu css dite hobe 56 | (bg-rose-50 text-gray-600 text-lg) 57 | 58 | 13. tarpor search er inpute field ke design korte hobe 59 | (bg-white/75 p-3 px-8 lg:w-96 rounded-full outline-none shadow-lg shadow-rose-100 focus:shadow-rose-200 duration-300) 60 | 61 | 14. ekon ul menu ke design korte hobe 62 | (flex gap-5) 63 | - navlink ke deign korte hobe 64 | (text-gray-400 hover:text-gray-600 duration-300) 65 | navlink er bitor je span tag ache oita ke o design korte hobe 66 | (text-bold text-sky-400) 67 | 68 | - active class dite navlink er bitore - navActive 69 | - return er upore navActive name ekta arrow function nite hobe and parameter hisebe ekta object nibe object er name hocche isActive click korle isActive ta trigger hobe 70 | - ei function ta return korbe r ekta object 71 | color: isActive ? "#f43f5e": null, 72 | - ekhane jokon e click kora tokon isActive ti trigger hobe and color ta show korbe otherwise color ta null dekabe 73 | 74 | 75 | 15. ekon input er jonno state nite hobe cz input e kono kichu like jeno sei data ta pawa jai tai nite hobe 76 | const [searchQuery, setSearchQuery] = useState(''); 77 | 78 | - tarpor input er bitor value dite hobe 79 | value = {searchQuery} 80 | - abr onChange() name ekta function o dite hobe jate kono kichu likar shate value ta change hoi setSearchQuery er maddome 81 | onChange(e) => setSearchQuery(e.target.value); 82 | 83 | - form er modde searchHandler name on submit add korte hobe jate inter dile form ta trigger hoi 84 | 85 | const searchHandler = (e) => { 86 | e.preventDefault(); 87 | 88 | console.log(searchQuery) 89 | } 90 | 91 | - input e ekta refrence nite hobe jate submit korle lika ta chole jai 92 | const inputFeildRef = useRef(null); 93 | ref={inputFeildRef} 94 | 95 | - setSearchQuery(""); 96 | inputFeildRef.current.blur(); 97 | 98 | r ei doi ta searchHandler er bitor add kore dite hobe 99 | 100 | 16. searchQuery jei value ta ache ekon sei ta navabr er bitor ache so amader ke ei searchQuery value ta ke home er bitor nite hobe 101 | - navabr e searchQuery jei state ta ache sei ta ke tule niye app.jsx e patate jate kore okhane theke navbar o home doi tai use kora jai mane stae ta ke top e niye jete hobe . abr eki bhave search handler ta ke o niye jete hobe . ekon niya jawar age amader ke ekta way follow korte hobe 102 | - first jeigula niye jaben sei gula ke props akare navabr paramater hisebe diye rakte hobe 103 | 104 | 105 | 17. getData name e ekta async function nite hobe jar parameter hisebe searchQuery ta nibe jar bitore data gula ke fetch korte hobe 106 | 107 | const getData = async (searchQuery) => { 108 | const res = await fetch(`https://forkify-api.herokuapp.com/api/search?q=${searchQuery}`); 109 | const data = await res.json(); 110 | console.log(data.recipes) 111 | } 112 | 113 | 18. fecth korar por jei data paichi tar jonno abar r ekta state nite hobe 114 | const [recipes, setRecipes] = useState([]); 115 | 116 | 19. jodi response property correct na thake tahole ekta error throw korte hobe 117 | if(!res.ok) throw new Error ('No receipe Found!'); 118 | 119 | 20. ato kon fetch kore jei data peyechi aita ke try{} er bitor rakte hobe 120 | const res = await fetch(`https://forkify-api.herokuapp.com/api/search?q=${searchQuery}`); 121 | if(!res.ok) throw new Error ('No reciepe Found'); 122 | const data = await res.json(); 123 | setRecipes(data.recipes) 124 | 125 | 126 | 21. try{} er niche obossoi catch(err){} use korte hobe . so erjonno abr 2 ta state extra add korte hobe 127 | const [loading, setLoading] = useState(false); 128 | const [error, setError] = useState(''); 129 | 130 | 22. ekon try er bitor setLoading(true) kore dite hobe . abar data set kore pore setLoading(flase) false kore dite hobe 131 | 132 | 23. ekon jodi error khai tai tahole catch block setError er message ta set kore dite hobe 133 | 134 | 24. 135 | const getData = async (searchQuery) => { 136 | try{ 137 | setLoading(true); 138 | const res = await fetch(`https://forkify-api.herokuapp.com/api/search?q=${searchQuery}`); 139 | if(!res.ok) throw new Error ('No reciepe Found'); 140 | const data = await res.json(); 141 | setRecipes(data.recipes); 142 | setLoading(false); 143 | } catch (err) { 144 | setError(err.message) 145 | } 146 | } 147 | 148 | ei conding ta ke ekta hook baniye tarpor reke dite hobe --------------------------------------------------------------------------------