├── postcss.config.js
├── tailwind.config.js
├── public
└── index.html
├── .gitignore
├── src
├── index.js
├── components
│ ├── Footer.jsx
│ ├── FryingPan.jsx
│ ├── Favourites.jsx
│ ├── NotFound.jsx
│ ├── Home.jsx
│ ├── Recipe.jsx
│ ├── Navbar.jsx
│ ├── RecipeItem.jsx
│ └── fryingPanStyle.css
├── index.css
├── hooks
│ └── useFetch.js
└── App.js
├── package.json
└── README.md
/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: ["./src/**/*.{js,jsx,ts,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Foodverse
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.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 |
13 | # misc
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
--------------------------------------------------------------------------------
/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/Footer.jsx:
--------------------------------------------------------------------------------
1 | const Footer = () => {
2 | return (
3 |
9 | );
10 | };
11 |
12 | export default Footer;
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/FryingPan.jsx:
--------------------------------------------------------------------------------
1 | import "./fryingPanStyle.css";
2 | const FryingPan = () => {
3 | return (
4 |
5 |
6 |
7 |
8 |
9 |
10 |
19 |
20 | );
21 | };
22 | export default FryingPan;
23 |
--------------------------------------------------------------------------------
/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;
22 |
--------------------------------------------------------------------------------
/src/components/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import FryingPan from "./FryingPan";
3 |
4 | const NotFound = () => {
5 | return (
6 |
7 |
8 | Page not found!
9 |
10 |
14 | Go home
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default NotFound;
22 |
--------------------------------------------------------------------------------
/src/components/Home.jsx:
--------------------------------------------------------------------------------
1 | import FryingPan from "./FryingPan";
2 | import Recipe from "./Recipe";
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;
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "foodverse",
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-router-dom": "^6.5.0",
12 | "react-scripts": "5.0.1",
13 | "web-vitals": "^2.1.4"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": [
23 | "react-app",
24 | "react-app/jest"
25 | ]
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | },
39 | "devDependencies": {
40 | "autoprefixer": "^10.4.13",
41 | "postcss": "^8.4.20",
42 | "tailwindcss": "^3.2.4"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # foodverse
2 |
3 | Foodverse GitHub repository - Source code for the food-focused website Foodverse.com. The site showcases recipes, cooking tips, and engaging food-related articles, all designed to provide a comprehensive and user-friendly experience. Built with HTML, CSS, and JavaScript for a responsive design on all devices.
4 |
5 | # Features
6 | - Foodverse is an innovative recipe web application catering to diverse culinary preferences
7 | - With an extensive collection of recipes for various cuisines and dietary needs.
8 | - Users can easily search, access ingredients, and save their favorite recipes.
9 | - Seamlessly plan meals, explore trending dishes, your ultimate recipe companion.
10 |
11 | # Tools and Technologies
12 | - React for building the user interface.
13 | - Tailwind Css for styling.
14 |
15 | # Installation
16 | 1. Clone the repository:`https://github.com/Shm-Rsuf/foodverse`
17 | 2. Navigate to the client directory: `cd foodverse`
18 | 3. Install dependencies: `npm install`
19 | 4. Start the client: `npm start`
20 | 5. Access the client in your browser at: `https://localhost:3000`
21 |
22 | # Links
23 | - [Live-Link](https://foodverse-a3.netlify.app/)
24 | - [Front-End-Link](https://github.com/Shm-Rsuf/foodverse)
25 |
--------------------------------------------------------------------------------
/src/components/Recipe.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | const Recipe = ({ recipe }) => {
4 | return (
5 |
6 |
7 |

12 |
13 |
14 |
15 |
16 | {recipe.publisher}
17 |
18 |
19 | {recipe.title}
20 |
21 |
25 | View recipe
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default Recipe;
33 |
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from "react-router-dom";
2 |
3 | const Navbar = ({
4 | searchQuery,
5 | setSearchQuery,
6 | searchHandler,
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 |
32 |
33 | -
34 |
40 | Home
41 |
42 |
43 |
44 | -
45 |
50 | Favourites{" "}
51 |
52 | ({savedItems.length})
53 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default Navbar;
62 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import { Route, Routes, useNavigate } from "react-router-dom";
3 |
4 | import Home from "./components/Home";
5 | import Navbar from "./components/Navbar";
6 | import Footer from "./components/Footer";
7 | import Favourites from "./components/Favourites";
8 | import NotFound from "./components/NotFound";
9 | import RecipeItem from "./components/RecipeItem";
10 |
11 | function App() {
12 | const [searchQuery, setSearchQuery] = useState("");
13 | const [recipes, setRecipes] = useState([]);
14 | const [loading, setLoading] = useState(false);
15 | const [error, setError] = useState("");
16 | const [savedItems, setSavedItems] = useState(() => {
17 | const localData = localStorage.getItem("recipes");
18 | return localData ? JSON.parse(localData) : [];
19 | });
20 |
21 | const inputField = useRef(null);
22 | const navigate = useNavigate();
23 |
24 | const searchHandler = (e) => {
25 | e.preventDefault();
26 | getData(searchQuery);
27 |
28 | setSearchQuery("");
29 | inputField.current.blur();
30 | setRecipes([]);
31 | setError("");
32 | navigate("/");
33 | };
34 |
35 | const getData = async (searchQuery) => {
36 | try {
37 | setLoading(true);
38 | const res = await fetch(
39 | `https://forkify-api.herokuapp.com/api/v2/recipes?search=${searchQuery}`
40 | );
41 | if (!res.ok) throw new Error("something went wrong!");
42 |
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 (error) {
48 | setError(error.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 |
107 |
108 | >
109 | );
110 | }
111 |
112 | export default App;
113 |
--------------------------------------------------------------------------------
/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 | const { data: recipe, loading, error } = useFetch(id);
9 |
10 | const durationCalc = (duration) => {
11 | if (!duration) return;
12 |
13 | if (!String(duration).includes(".")) {
14 | return duration + "h";
15 | }
16 |
17 | if (String(duration).includes(".")) {
18 | const splittedDuration = String(duration).split(".");
19 | const hour = splittedDuration[0] + "h";
20 | const splitterMinutes = "." + splittedDuration[1];
21 | const minutes = +splitterMinutes * 60 + "min";
22 |
23 | return hour + minutes;
24 | }
25 | };
26 |
27 | useEffect(() => {
28 | if (!recipe) return;
29 |
30 | setItemSavedStatus(savedItems.some((item) => item.id === recipe.id));
31 | }, [recipe]);
32 |
33 | return (
34 |
35 |
36 |
37 |

42 |
43 |
44 |
45 | Ingredients:
46 |
47 |
48 | {recipe?.ingredients?.map((ing, i) => (
49 | -
50 | ✓ {ing.quantity}
51 | {ing.unit} {ing.description}
52 |
53 | ))}
54 |
55 |
56 |
57 |
58 |
59 | {recipe?.publisher}
60 |
61 |
{recipe?.title}
62 |
63 |
Servings: {recipe?.servings} people
64 |
65 | Cooking time:{" "}
66 | {recipe?.cooking_time < 60
67 | ? String(recipe?.cooking_time) + "min"
68 | : durationCalc(recipe?.cooking_time / 60)}
69 |
70 |
71 |
72 |
84 |
90 | Get directions
91 |
92 |
96 | Back to home
97 |
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | export default RecipeItem;
105 |
--------------------------------------------------------------------------------
/src/components/fryingPanStyle.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 | }
240 |
--------------------------------------------------------------------------------