├── .gitignore
├── .parcelrc
├── .postcssrc
├── README.md
├── babel.config.js
├── index.css
├── index.html
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── App.js
├── components
│ ├── About.js
│ ├── Body.js
│ ├── Cart.js
│ ├── Contact.js
│ ├── Error.js
│ ├── Grocery.js
│ ├── Header.js
│ ├── ItemList.js
│ ├── RestaurantCard.js
│ ├── RestaurantCategory.js
│ ├── RestaurantMenu.js
│ ├── Shimmer.js
│ ├── User.js
│ ├── UserClass.js
│ ├── __tests__
│ │ ├── Cart.test.js
│ │ ├── Contact.test.js
│ │ ├── Header.test.js
│ │ ├── RestaurantCard.test.js
│ │ ├── Search.test.js
│ │ └── sum.test.js
│ ├── mocks
│ │ ├── mockResListData.json
│ │ ├── mockResMenu.json
│ │ └── resCardMock.json
│ └── sum.js
└── utils
│ ├── UserContext.js
│ ├── appStore.js
│ ├── cartSlice.js
│ ├── constants.js
│ ├── useOnlineStatus.js
│ └── useRestrauntMenu.js
└── tailwind.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | .parcel-cache
4 | /coverage
--------------------------------------------------------------------------------
/.parcelrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@parcel/config-default",
3 | "transformers": {
4 | "*.{js,mjs,jsx,cjs,ts,tsx}": [
5 | "@parcel/transformer-js",
6 | "@parcel/transformer-react-refresh-wrap"
7 | ]
8 | }
9 | }
--------------------------------------------------------------------------------
/.postcssrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "tailwindcss": {}
4 | }
5 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Namaste React 🚀
2 |
3 |
4 | # Parcel
5 | - Dev Build
6 | - Local Server
7 | - HMR = Hot Module Replacement
8 | - File Watching Algorithm - written in C++
9 | - Caching - Faster Builds
10 | - Image Optimization
11 | - Minification
12 | - Bundling
13 | - Compress
14 | - Consistent Hashing
15 | - Code Splitting
16 | - Differential Bundling - support older browsers
17 | - Diagnostic
18 | - Error Handling
19 | - HTTPs
20 | - Tree Shaking - remove unused code
21 | - Different dev and prod bundles
22 |
23 |
24 |
25 | # Namaste Food
26 |
27 |
28 | /**
29 | * Header
30 | * - Logo
31 | * - Nav Items
32 | * Body
33 | * - Search
34 | * - RestaurantContainer
35 | * - RestaurantCard
36 | * - Img
37 | * - Name of Res, Star Rating, cuisine, delery tie
38 | * Footer
39 | * - Copyright
40 | * - Links
41 | * - Address
42 | * - Contact
43 | */
44 |
45 |
46 |
47 | Two types of Export/Import
48 |
49 |
50 | - Default Export/Import
51 |
52 | export default Component;
53 | import Component from "path";
54 |
55 |
56 | - Named Export/Import
57 |
58 | export const Component;
59 | import {Component} from "path";
60 |
61 |
62 | # React Hooks
63 | (Normal JS utility functions)
64 | - useState() - Superpowerful State Variables in react
65 | - useEffect()
66 |
67 |
68 |
69 | # 2 types Routing in web apps
70 | - Client Side Routing
71 | - Server Side Routing
72 |
73 |
74 |
75 |
76 | # Redux Toolkit
77 | - Install @reduxjs/toolkit and react-redux
78 | - Build our store
79 | - Connect our store to our app
80 | - Slice (cartSlice)
81 | - dispatch(action)
82 | - Selector
83 |
84 |
85 | # Types of testing (devloper)
86 | - Unit Testing
87 | - Integration Testing
88 | - End to End Testing - e2e testing
89 |
90 | # Setting up Testing in our app
91 | - Install React Testing Library
92 | - Installed jest
93 | - Installed Babel dependencies
94 | - Configure Babel
95 | - Configure Parcel Config file to disable default babel transpilation
96 | - Jest - npx jest --init
97 | - Install jsdom library
98 | - Install @babel/preset-react - to make JSX work in test cases
99 | - Include @babel/preset-react inside my babel config
100 | - npm i -D @testing-library/jest-dom
101 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ["@babel/preset-env", { targets: { node: "current" } }],
4 | ["@babel/preset-react", { runtime: "automatic" }],
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Namaste React
9 |
10 |
11 |
12 |
Not Rendered
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | /** @type {import('jest').Config} */
7 | const config = {
8 | // All imported modules in your tests should be mocked automatically
9 | // automock: false,
10 |
11 | // Stop running tests after `n` failures
12 | // bail: 0,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/private/var/folders/dp/nxmqylfs253fb0w1z3zkk9900000gn/T/jest_dx",
16 |
17 | // Automatically clear mock calls, instances, contexts and results before every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | collectCoverage: true,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: undefined,
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: "coverage",
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | // coveragePathIgnorePatterns: [
31 | // "/node_modules/"
32 | // ],
33 |
34 | // Indicates which provider should be used to instrument code for coverage
35 | // coverageProvider: "babel",
36 |
37 | // A list of reporter names that Jest uses when writing coverage reports
38 | // coverageReporters: [
39 | // "json",
40 | // "text",
41 | // "lcov",
42 | // "clover"
43 | // ],
44 |
45 | // An object that configures minimum threshold enforcement for coverage results
46 | // coverageThreshold: undefined,
47 |
48 | // A path to a custom dependency extractor
49 | // dependencyExtractor: undefined,
50 |
51 | // Make calling deprecated APIs throw helpful error messages
52 | // errorOnDeprecated: false,
53 |
54 | // The default configuration for fake timers
55 | // fakeTimers: {
56 | // "enableGlobally": false
57 | // },
58 |
59 | // Force coverage collection from ignored files using an array of glob patterns
60 | // forceCoverageMatch: [],
61 |
62 | // A path to a module which exports an async function that is triggered once before all test suites
63 | // globalSetup: undefined,
64 |
65 | // A path to a module which exports an async function that is triggered once after all test suites
66 | // globalTeardown: undefined,
67 |
68 | // A set of global variables that need to be available in all test environments
69 | // globals: {},
70 |
71 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
72 | // maxWorkers: "50%",
73 |
74 | // An array of directory names to be searched recursively up from the requiring module's location
75 | // moduleDirectories: [
76 | // "node_modules"
77 | // ],
78 |
79 | // An array of file extensions your modules use
80 | // moduleFileExtensions: [
81 | // "js",
82 | // "mjs",
83 | // "cjs",
84 | // "jsx",
85 | // "ts",
86 | // "tsx",
87 | // "json",
88 | // "node"
89 | // ],
90 |
91 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
92 | // moduleNameMapper: {},
93 |
94 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
95 | // modulePathIgnorePatterns: [],
96 |
97 | // Activates notifications for test results
98 | // notify: false,
99 |
100 | // An enum that specifies notification mode. Requires { notify: true }
101 | // notifyMode: "failure-change",
102 |
103 | // A preset that is used as a base for Jest's configuration
104 | // preset: undefined,
105 |
106 | // Run tests from one or more projects
107 | // projects: undefined,
108 |
109 | // Use this configuration option to add custom reporters to Jest
110 | // reporters: undefined,
111 |
112 | // Automatically reset mock state before every test
113 | // resetMocks: false,
114 |
115 | // Reset the module registry before running each individual test
116 | // resetModules: false,
117 |
118 | // A path to a custom resolver
119 | // resolver: undefined,
120 |
121 | // Automatically restore mock state and implementation before every test
122 | // restoreMocks: false,
123 |
124 | // The root directory that Jest should scan for tests and modules within
125 | // rootDir: undefined,
126 |
127 | // A list of paths to directories that Jest should use to search for files in
128 | // roots: [
129 | // ""
130 | // ],
131 |
132 | // Allows you to use a custom runner instead of Jest's default test runner
133 | // runner: "jest-runner",
134 |
135 | // The paths to modules that run some code to configure or set up the testing environment before each test
136 | // setupFiles: [],
137 |
138 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
139 | // setupFilesAfterEnv: [],
140 |
141 | // The number of seconds after which a test is considered as slow and reported as such in the results.
142 | // slowTestThreshold: 5,
143 |
144 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
145 | // snapshotSerializers: [],
146 |
147 | // The test environment that will be used for testing
148 | testEnvironment: "jsdom",
149 |
150 | // Options that will be passed to the testEnvironment
151 | // testEnvironmentOptions: {},
152 |
153 | // Adds a location field to test results
154 | // testLocationInResults: false,
155 |
156 | // The glob patterns Jest uses to detect test files
157 | // testMatch: [
158 | // "**/__tests__/**/*.[jt]s?(x)",
159 | // "**/?(*.)+(spec|test).[tj]s?(x)"
160 | // ],
161 |
162 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
163 | // testPathIgnorePatterns: [
164 | // "/node_modules/"
165 | // ],
166 |
167 | // The regexp pattern or array of patterns that Jest uses to detect test files
168 | // testRegex: [],
169 |
170 | // This option allows the use of a custom results processor
171 | // testResultsProcessor: undefined,
172 |
173 | // This option allows use of a custom test runner
174 | // testRunner: "jest-circus/runner",
175 |
176 | // A map from regular expressions to paths to transformers
177 | // transform: undefined,
178 |
179 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
180 | // transformIgnorePatterns: [
181 | // "/node_modules/",
182 | // "\\.pnp\\.[^\\/]+$"
183 | // ],
184 |
185 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
186 | // unmockedModulePathPatterns: undefined,
187 |
188 | // Indicates whether each individual test should be reported during the run
189 | // verbose: undefined,
190 |
191 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
192 | // watchPathIgnorePatterns: [],
193 |
194 | // Whether to use watchman for file crawling
195 | // watchman: true,
196 | };
197 |
198 | module.exports = config;
199 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "namaste-react",
3 | "version": "1.0.0",
4 | "description": "This is Namaste React by Akshay Saini",
5 | "scripts": {
6 | "start": "parcel index.html",
7 | "build": "parcel build index.html",
8 | "test": "jest",
9 | "watch-test": "jest --watch"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/namastedev/namaste-react.git"
14 | },
15 | "keywords": [],
16 | "author": "Akshay Saini",
17 | "license": "ISC",
18 | "bugs": {
19 | "url": "https://github.com/namastedev/namaste-react/issues"
20 | },
21 | "homepage": "https://github.com/namastedev/namaste-react#readme",
22 | "devDependencies": {
23 | "@babel/core": "^7.22.9",
24 | "@babel/preset-env": "^7.22.9",
25 | "@babel/preset-react": "^7.22.5",
26 | "@testing-library/jest-dom": "^5.17.0",
27 | "@testing-library/react": "^14.0.0",
28 | "babel-jest": "^29.6.2",
29 | "jest": "^29.6.2",
30 | "jest-environment-jsdom": "^29.6.2",
31 | "parcel": "^2.8.3",
32 | "postcss": "^8.4.24",
33 | "process": "^0.11.10",
34 | "tailwindcss": "^3.3.2"
35 | },
36 | "dependencies": {
37 | "@reduxjs/toolkit": "^1.9.5",
38 | "react": "^18.2.0",
39 | "react-dom": "^18.2.0",
40 | "react-redux": "^8.1.1",
41 | "react-router-dom": "^6.11.2"
42 | },
43 | "browserslist": [
44 | "last 2 versions"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { lazy, Suspense, useEffect, useState } from "react";
2 | import ReactDOM from "react-dom/client";
3 | import Header from "./components/Header";
4 | import Body from "./components/Body";
5 | //import About from "./components/About";
6 | import Contact from "./components/Contact";
7 | import Error from "./components/Error";
8 | import RestaurantMenu from "./components/RestaurantMenu";
9 | import { createBrowserRouter, RouterProvider, Outlet } from "react-router-dom";
10 | import UserContext from "./utils/UserContext";
11 | import { Provider } from "react-redux";
12 | import appStore from "./utils/appStore";
13 | import Cart from "./components/Cart";
14 |
15 | //import Grocery from "./components/Grocery";
16 |
17 | // Chunking
18 | // Code Splitting
19 | // Dynamic Bundling
20 | // lazy Loading
21 | // on demand loading
22 | // dynamix imoprt
23 |
24 | const Grocery = lazy(() => import("./components/Grocery"));
25 |
26 | const About = lazy(() => import("./components/About"));
27 |
28 | const AppLayout = () => {
29 | const [userName, setUserName] = useState();
30 |
31 | //authentication
32 | useEffect(() => {
33 | // Make an API call and send username and password
34 | const data = {
35 | name: "Akshay Saini",
36 | };
37 | setUserName(data.name);
38 | }, []);
39 |
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | const appRouter = createBrowserRouter([
53 | {
54 | path: "/",
55 | element: ,
56 | children: [
57 | {
58 | path: "/",
59 | element: ,
60 | },
61 | {
62 | path: "/about",
63 | element: (
64 | Loading....}>
65 |
66 |
67 | ),
68 | },
69 | {
70 | path: "/contact",
71 | element: ,
72 | },
73 | {
74 | path: "/grocery",
75 | element: (
76 | Loading....}>
77 |
78 |
79 | ),
80 | },
81 | {
82 | path: "/restaurants/:resId",
83 | element: ,
84 | },
85 | {
86 | path: "/cart",
87 | element: ,
88 | },
89 | ],
90 | errorElement: ,
91 | },
92 | ]);
93 |
94 | const root = ReactDOM.createRoot(document.getElementById("root"));
95 |
96 | root.render( );
97 |
--------------------------------------------------------------------------------
/src/components/About.js:
--------------------------------------------------------------------------------
1 | import User from "./User";
2 | import UserClass from "./UserClass";
3 | import { Component } from "react";
4 | import UserContext from "../utils/UserContext";
5 |
6 | class About extends Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | //console.log("Parent Constructor");
11 | }
12 |
13 | componentDidMount() {
14 | //console.log("Parent Component Did Mount");
15 | }
16 |
17 | render() {
18 | //console.log("Parent Render");
19 |
20 | return (
21 |
22 |
About Class Component
23 |
24 | LoggedIn User
25 |
26 | {({ loggedInUser }) => (
27 | {loggedInUser}
28 | )}
29 |
30 |
31 |
This is Namaste React Web Series
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export default About;
39 |
--------------------------------------------------------------------------------
/src/components/Body.js:
--------------------------------------------------------------------------------
1 | import RestaurantCard, { withPromtedLabel } from "./RestaurantCard";
2 | import { useState, useEffect, useContext } from "react";
3 | import Shimmer from "./Shimmer";
4 | import { Link } from "react-router-dom";
5 | import useOnlineStatus from "../utils/useOnlineStatus";
6 | import UserContext from "../utils/UserContext";
7 |
8 | const Body = () => {
9 | // Local State Variable - Super powerful variable
10 | const [listOfRestaurants, setListOfRestraunt] = useState([]);
11 | const [filteredRestaurant, setFilteredRestaurant] = useState([]);
12 |
13 | const [searchText, setSearchText] = useState("");
14 |
15 | const RestaurantCardPromoted = withPromtedLabel(RestaurantCard);
16 |
17 | // Whenever state variables update, react triggers a reconciliation cycle(re-renders the component)
18 |
19 | useEffect(() => {
20 | fetchData();
21 | }, []);
22 |
23 | const fetchData = async () => {
24 | const data = await fetch(
25 | "https://www.swiggy.com/dapi/restaurants/list/v5?lat=12.9351929&lng=77.62448069999999&page_type=DESKTOP_WEB_LISTING"
26 | );
27 |
28 | const json = await data.json();
29 |
30 | // Optional Chaining
31 | setListOfRestraunt(
32 | json?.data?.cards[2]?.card?.card?.gridElements?.infoWithStyle?.restaurants
33 | );
34 | setFilteredRestaurant(
35 | json?.data?.cards[2]?.card?.card?.gridElements?.infoWithStyle?.restaurants
36 | );
37 | };
38 |
39 | const onlineStatus = useOnlineStatus();
40 |
41 | if (onlineStatus === false)
42 | return (
43 |
44 | Looks like you're offline!! Please check your internet connection;
45 |
46 | );
47 |
48 | const { loggedInUser, setUserName } = useContext(UserContext);
49 |
50 | return listOfRestaurants.length === 0 ? (
51 |
52 | ) : (
53 |
54 |
55 |
56 | {
62 | setSearchText(e.target.value);
63 | }}
64 | />
65 | {
68 | // Filter the restraunt cards and update the UI
69 | // searchText
70 | console.log(searchText);
71 |
72 | const filteredRestaurant = listOfRestaurants.filter((res) =>
73 | res.info.name.toLowerCase().includes(searchText.toLowerCase())
74 | );
75 |
76 | setFilteredRestaurant(filteredRestaurant);
77 | }}
78 | >
79 | Search
80 |
81 |
82 |
83 | {
86 | const filteredList = listOfRestaurants.filter(
87 | (res) => res.info.avgRating > 4
88 | );
89 | setFilteredRestaurant(filteredList);
90 | }}
91 | >
92 | Top Rated Restaurants
93 |
94 |
95 |
96 | UserName :
97 | setUserName(e.target.value)}
101 | />
102 |
103 |
104 |
105 | {filteredRestaurant.map((restaurant) => (
106 |
110 | {restaurant?.info.promoted ? (
111 |
112 | ) : (
113 |
114 | )}
115 |
116 | ))}
117 |
118 |
119 | );
120 | };
121 |
122 | export default Body;
123 |
--------------------------------------------------------------------------------
/src/components/Cart.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { clearCart } from "../utils/cartSlice";
3 | import ItemList from "./ItemList";
4 | import { useDispatch } from "react-redux";
5 |
6 | const Cart = () => {
7 | const cartItems = useSelector((store) => store.cart.items);
8 |
9 | console.log(cartItems);
10 |
11 | const dispatch = useDispatch();
12 |
13 | const handleClearCart = () => {
14 | dispatch(clearCart());
15 | };
16 |
17 | return (
18 |
19 |
Cart
20 |
21 |
25 | Clear Cart
26 |
27 | {cartItems?.length === 0 && (
28 |
Cart is empty. Add Items to the cart!
29 | )}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default Cart;
37 |
--------------------------------------------------------------------------------
/src/components/Contact.js:
--------------------------------------------------------------------------------
1 | const Contact = () => {
2 | return (
3 |
4 |
Contact Us Page
5 |
20 |
21 | );
22 | };
23 | export default Contact;
24 |
--------------------------------------------------------------------------------
/src/components/Error.js:
--------------------------------------------------------------------------------
1 | import { useRouteError } from "react-router-dom";
2 |
3 | const Error = () => {
4 | const err = useRouteError();
5 | console.log(err);
6 | return (
7 |
8 |
Oops!!!
9 | Something went wrong!!
10 |
11 | {err.status}: {err.statusText}
12 |
13 |
14 | );
15 | };
16 |
17 | export default Error;
18 |
--------------------------------------------------------------------------------
/src/components/Grocery.js:
--------------------------------------------------------------------------------
1 | const Grocery = () => {
2 | return (
3 |
4 | {" "}
5 | Our grocery online store, and we have a lot of child components inside
6 | this web page!!!
7 |
8 | );
9 | };
10 |
11 | export default Grocery;
12 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import { LOGO_URL } from "../utils/constants";
2 | import { useState, useContext } from "react";
3 | import { Link } from "react-router-dom";
4 | import useOnlineStatus from "../utils/useOnlineStatus";
5 | import UserContext from "../utils/UserContext";
6 | import { useSelector } from "react-redux";
7 |
8 | const Header = () => {
9 | const [btnNameReact, setBtnNameReact] = useState("Login");
10 |
11 | const onlineStatus = useOnlineStatus();
12 |
13 | const { loggedInUser } = useContext(UserContext);
14 | //console.log(loggedInUser);
15 |
16 | // Subscribing to the store using a Selector
17 | const cartItems = useSelector((store) => store.cart.items);
18 | //console.log(cartItems);
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 | Online Status: {onlineStatus ? "✅" : "🔴"}
28 |
29 | Home
30 |
31 |
32 | About Us
33 |
34 |
35 | Contact Us
36 |
37 |
38 | Grocery
39 |
40 |
41 | Cart - ({cartItems.length} items)
42 |
43 | {
46 | btnNameReact === "Login"
47 | ? setBtnNameReact("Logout")
48 | : setBtnNameReact("Login");
49 | }}
50 | >
51 | {btnNameReact}
52 |
53 |
54 | {loggedInUser}
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default Header;
62 |
--------------------------------------------------------------------------------
/src/components/ItemList.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { addItem } from "../utils/cartSlice";
3 | import { CDN_URL } from "../utils/constants";
4 |
5 | const ItemList = ({ items, dummy }) => {
6 | const dispatch = useDispatch();
7 |
8 | const handleAddItem = (item) => {
9 | // Dispatch an action
10 | dispatch(addItem(item));
11 | };
12 |
13 | return (
14 |
15 | {items.map((item) => (
16 |
21 |
22 |
23 | {item.card.info.name}
24 |
25 | - ₹
26 | {item.card.info.price
27 | ? item.card.info.price / 100
28 | : item.card.info.defaultPrice / 100}
29 |
30 |
31 |
{item.card.info.description}
32 |
33 |
34 |
35 | handleAddItem(item)}
38 | >
39 | Add +
40 |
41 |
42 |
43 |
44 |
45 | ))}
46 |
47 | );
48 | };
49 |
50 | export default ItemList;
51 |
--------------------------------------------------------------------------------
/src/components/RestaurantCard.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { CDN_URL } from "../utils/constants";
3 | import UserContext from "../utils/UserContext";
4 |
5 | const RestaurantCard = (props) => {
6 | const { resData } = props;
7 | const { loggedInUser } = useContext(UserContext);
8 |
9 | const {
10 | cloudinaryImageId,
11 | name,
12 | avgRating,
13 | cuisines,
14 | costForTwo,
15 | deliveryTime,
16 | } = resData;
17 |
18 | return (
19 |
23 |
28 |
{name}
29 |
{cuisines.join(", ")}
30 |
{avgRating} stars
31 |
₹{costForTwo / 100} FOR TWO
32 |
{deliveryTime} minutes
33 |
User : {loggedInUser}
34 |
35 | );
36 | };
37 |
38 | // Higher Order Component
39 |
40 | // input - RestaurantCard =>> RestaurantCardPromoted
41 |
42 | export const withPromtedLabel = (RestaurantCard) => {
43 | return (props) => {
44 | return (
45 |
46 |
47 | Promoted
48 |
49 |
50 |
51 | );
52 | };
53 | };
54 |
55 | export default RestaurantCard;
56 |
--------------------------------------------------------------------------------
/src/components/RestaurantCategory.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import ItemList from "./ItemList";
3 |
4 | const RestaurantCategory = ({ data, showItems, setShowIndex, dummy }) => {
5 | const handleClick = () => {
6 | setShowIndex();
7 | };
8 | return (
9 |
10 | {/* Header */}
11 |
12 |
16 |
17 | {data.title} ({data.itemCards.length})
18 |
19 | ⬇️
20 |
21 |
22 | {showItems &&
}
23 |
24 |
25 | );
26 | };
27 |
28 | export default RestaurantCategory;
29 |
--------------------------------------------------------------------------------
/src/components/RestaurantMenu.js:
--------------------------------------------------------------------------------
1 | import Shimmer from "./Shimmer";
2 | import { useParams } from "react-router-dom";
3 | import useRestaurantMenu from "../utils/useRestrauntMenu";
4 | import RestaurantCategory from "./RestaurantCategory";
5 | import { useState } from "react";
6 |
7 | const RestaurantMenu = () => {
8 | const { resId } = useParams();
9 |
10 | const dummy = "Dummy Data";
11 |
12 | const resInfo = useRestaurantMenu(resId);
13 |
14 | const [showIndex, setShowIndex] = useState(null);
15 |
16 | if (resInfo === null) return ;
17 |
18 | const { name, cuisines, costForTwoMessage } =
19 | resInfo?.cards[0]?.card?.card?.info;
20 |
21 | const { itemCards } =
22 | resInfo?.cards[2]?.groupedCard?.cardGroupMap?.REGULAR?.cards[1]?.card?.card;
23 |
24 | const categories =
25 | resInfo?.cards[2]?.groupedCard?.cardGroupMap?.REGULAR?.cards.filter(
26 | (c) =>
27 | c.card?.["card"]?.["@type"] ===
28 | "type.googleapis.com/swiggy.presentation.food.v2.ItemCategory"
29 | );
30 | //console.log(categories);
31 |
32 | return (
33 |
34 |
{name}
35 |
36 | {cuisines.join(", ")} - {costForTwoMessage}
37 |
38 | {/* categories accordions */}
39 | {categories.map((category, index) => (
40 | // controlled component
41 |
setShowIndex(index)}
46 | dummy={dummy}
47 | />
48 | ))}
49 |
50 | );
51 | };
52 |
53 | export default RestaurantMenu;
54 |
--------------------------------------------------------------------------------
/src/components/Shimmer.js:
--------------------------------------------------------------------------------
1 | const Shimmer = () => {
2 | return (
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default Shimmer;
27 |
--------------------------------------------------------------------------------
/src/components/User.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | const User = ({ name }) => {
4 | const [count, setCount] = useState(0);
5 | const [count2] = useState(1);
6 |
7 | useEffect(() => {
8 | // Api Calls
9 | }, []);
10 |
11 | return (
12 |
13 |
Count = {count}
14 | Count2 = {count2}
15 | Name: {name}
16 | Location: Dehradun
17 | Contact: @akshaymarch7
18 |
19 | );
20 | };
21 |
22 | export default User;
23 |
--------------------------------------------------------------------------------
/src/components/UserClass.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | class UserClass extends React.Component {
4 | constructor(props) {
5 | super(props);
6 |
7 | this.state = {
8 | userInfo: {
9 | name: "Dummy",
10 | location: "Default",
11 | },
12 | };
13 | //console.log(this.props.name + "Child Constructor");
14 | }
15 |
16 | async componentDidMount() {
17 | //console.log(this.props.name + "Child Component Did Mount");
18 | // Api call
19 |
20 | const data = await fetch("https://api.github.com/users/akshaymarch7");
21 | const json = await data.json();
22 |
23 | this.setState({
24 | userInfo: json,
25 | });
26 |
27 | //console.log(json);
28 | }
29 |
30 | componentDidUpdate() {
31 | //console.log("Component Did Update");
32 | }
33 |
34 | componentWillUnmount() {
35 | //console.log("Component Will Unmount");
36 | }
37 |
38 | render() {
39 | console.log(this.props.name + "Child Render");
40 |
41 | const { name, location, avatar_url } = this.state.userInfo;
42 | return (
43 |
44 |
45 |
Name: {name}
46 |
Location: {location}
47 |
Contact: @akshaymarch7
48 |
49 | );
50 | }
51 | }
52 |
53 | export default UserClass;
54 |
55 | /****
56 | *
57 | * --- MOUNTING ----
58 | *
59 | * Constructor (dummy)
60 | * Render (dummy)
61 | *
62 | * Component Did MOunt
63 | *
64 | * -> State variable is updated
65 | *
66 | * ---- UPDATE
67 | *
68 | * render(APi data)
69 | * )
70 | * ccomponentDid Update
71 | *
72 | *
73 | *
74 | *
75 | */
76 |
--------------------------------------------------------------------------------
/src/components/__tests__/Cart.test.js:
--------------------------------------------------------------------------------
1 | import { fireEvent, render, screen } from "@testing-library/react";
2 | import { act } from "react-dom/test-utils";
3 | import RestaurantMenu from "../RestaurantMenu";
4 | import Header from "../Header";
5 | import Cart from "../Cart";
6 | import MOCK_DATA_NAME from "../mocks/mockResMenu.json";
7 | import { Provider } from "react-redux";
8 | import appStore from "../../utils/appStore";
9 | import { BrowserRouter } from "react-router-dom";
10 | import "@testing-library/jest-dom";
11 |
12 | global.fetch = jest.fn(() =>
13 | Promise.resolve({
14 | json: () => Promise.resolve(MOCK_DATA_NAME),
15 | })
16 | );
17 |
18 | it("should Load Restaurant Menu Component", async () => {
19 | await act(async () =>
20 | render(
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 | );
30 |
31 | const accordionHeader = screen.getByText("Biriyani (5)");
32 | fireEvent.click(accordionHeader);
33 |
34 | expect(screen.getAllByTestId("foodItems").length).toBe(5);
35 |
36 | expect(screen.getByText("Cart - (0 items)")).toBeInTheDocument();
37 |
38 | const addBtns = screen.getAllByRole("button", { name: "Add +" });
39 | fireEvent.click(addBtns[0]);
40 |
41 | expect(screen.getByText("Cart - (1 items)")).toBeInTheDocument();
42 |
43 | fireEvent.click(addBtns[1]);
44 |
45 | expect(screen.getByText("Cart - (2 items)")).toBeInTheDocument();
46 |
47 | expect(screen.getAllByTestId("foodItems").length).toBe(7);
48 |
49 | fireEvent.click(screen.getByRole("button", { name: "Clear Cart" }));
50 |
51 | expect(screen.getAllByTestId("foodItems").length).toBe(5);
52 |
53 | expect(
54 | screen.getByText("Cart is empty. Add Items to the cart!")
55 | ).toBeInTheDocument();
56 | });
57 |
--------------------------------------------------------------------------------
/src/components/__tests__/Contact.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import Contact from "../Contact";
3 | import "@testing-library/jest-dom";
4 |
5 | describe("Contact Us Page Test Case", () => {
6 | // beforeAll(() => {
7 | // console.log("Before All");
8 | // });
9 |
10 | // beforeEach(() => {
11 | // console.log("Before Each");
12 | // });
13 |
14 | // afterAll(() => {
15 | // console.log("After All");
16 | // });
17 |
18 | // afterEach(() => {
19 | // console.log("After Each");
20 | // });
21 |
22 | it("Should load contact us component", () => {
23 | render( );
24 |
25 | const heading = screen.getByRole("heading");
26 |
27 | // Assertion
28 | expect(heading).toBeInTheDocument();
29 | });
30 |
31 | it("Should load button inside Contact component", () => {
32 | render( );
33 |
34 | const button = screen.getByRole("button");
35 |
36 | // Assertion
37 | expect(button).toBeInTheDocument();
38 | });
39 |
40 | it("Should load input name inside Contact component", () => {
41 | render( );
42 |
43 | const inputName = screen.getByPlaceholderText("name");
44 |
45 | // Assertion
46 | expect(inputName).toBeInTheDocument();
47 | });
48 |
49 | it("Should load 2 input boxes on the Contact component", () => {
50 | render( );
51 |
52 | // Querying
53 | const inputBoxes = screen.getAllByRole("textbox");
54 |
55 | //console.log(inputBoxes.length);
56 |
57 | // Assertion
58 |
59 | expect(inputBoxes.length).toBe(2);
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/src/components/__tests__/Header.test.js:
--------------------------------------------------------------------------------
1 | import { fireEvent, render, screen } from "@testing-library/react";
2 | import { Provider } from "react-redux";
3 | import appStore from "../../utils/appStore";
4 | import Header from "../Header";
5 | import { BrowserRouter } from "react-router-dom";
6 | import "@testing-library/jest-dom";
7 |
8 | it("Should render Header Component with a login button", () => {
9 | render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | const loginButton = screen.getByRole("button", { name: "Login" });
18 |
19 | //const loginButton = screen.getByText("Login");
20 |
21 | expect(loginButton).toBeInTheDocument();
22 | });
23 |
24 | it("Should render Header Component with a Cart items 0 ", () => {
25 | render(
26 |
27 |
28 |
29 |
30 |
31 | );
32 |
33 | const cartItems = screen.getByText("Cart - (0 items)");
34 |
35 | expect(cartItems).toBeInTheDocument();
36 | });
37 |
38 | it("Should render Header Component with a Cart item ", () => {
39 | render(
40 |
41 |
42 |
43 |
44 |
45 | );
46 |
47 | const cartItems = screen.getByText(/Cart/);
48 |
49 | expect(cartItems).toBeInTheDocument();
50 | });
51 |
52 | it("Should change Login Button to Logout on click", () => {
53 | render(
54 |
55 |
56 |
57 |
58 |
59 | );
60 |
61 | const loginButton = screen.getByRole("button", { name: "Login" });
62 |
63 | fireEvent.click(loginButton);
64 |
65 | const logoutButton = screen.getByRole("button", { name: "Logout" });
66 |
67 | expect(logoutButton).toBeInTheDocument();
68 | });
69 |
--------------------------------------------------------------------------------
/src/components/__tests__/RestaurantCard.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import RestaurantCard from "../RestaurantCard";
3 | import MOCK_DATA from "../mocks/resCardMock.json";
4 | import "@testing-library/jest-dom";
5 |
6 | it("should render RestaurantCard component with props Data", () => {
7 | render( );
8 |
9 | const name = screen.getByText("Leon's - Burgers & Wings (Leon Grill)");
10 |
11 | expect(name).toBeInTheDocument();
12 | });
13 |
14 | it("should render RestaurantCard component with Promoted Label", () => {
15 | // Home Work - test HOC : withPromtedLabel()
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/__tests__/Search.test.js:
--------------------------------------------------------------------------------
1 | import { fireEvent, render, screen } from "@testing-library/react";
2 | import { act } from "react-dom/test-utils";
3 | import Body from "../Body";
4 | import MOCK_DATA from "../mocks/mockResListData.json";
5 | import { BrowserRouter } from "react-router-dom";
6 | import "@testing-library/jest-dom";
7 |
8 | global.fetch = jest.fn(() => {
9 | return Promise.resolve({
10 | json: () => {
11 | return Promise.resolve(MOCK_DATA);
12 | },
13 | });
14 | });
15 |
16 | it("Should Search Res List for burger text input ", async () => {
17 | await act(async () =>
18 | render(
19 |
20 |
21 |
22 | )
23 | );
24 |
25 | const cardsBeforeSearch = screen.getAllByTestId("resCard");
26 |
27 | expect(cardsBeforeSearch.length).toBe(20);
28 |
29 | const searchBtn = screen.getByRole("button", { name: "Search" });
30 |
31 | const searchInput = screen.getByTestId("searchInput");
32 |
33 | fireEvent.change(searchInput, { target: { value: "burger" } });
34 |
35 | fireEvent.click(searchBtn);
36 |
37 | const cardsAfterSearch = screen.getAllByTestId("resCard");
38 |
39 | expect(cardsAfterSearch.length).toBe(4);
40 | });
41 |
42 | it("Should filter Top Rated Restaurant", async () => {
43 | await act(async () =>
44 | render(
45 |
46 |
47 |
48 | )
49 | );
50 |
51 | const cardsBeforeFilter = screen.getAllByTestId("resCard");
52 |
53 | expect(cardsBeforeFilter.length).toBe(20);
54 |
55 | const topRatedBtn = screen.getByRole("button", {
56 | name: "Top Rated Restaurants",
57 | });
58 | fireEvent.click(topRatedBtn);
59 |
60 | const cardsAfterFilter = screen.getAllByTestId("resCard");
61 | expect(cardsAfterFilter.length).toBe(13);
62 | });
63 |
--------------------------------------------------------------------------------
/src/components/__tests__/sum.test.js:
--------------------------------------------------------------------------------
1 | import { sum } from "../sum";
2 |
3 | test("Sum function should caculate the sum of two numbers", () => {
4 | const result = sum(3, 4);
5 |
6 | //Assertion
7 | expect(result).toBe(7);
8 | });
9 |
--------------------------------------------------------------------------------
/src/components/mocks/resCardMock.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "65797",
3 | "name": "Leon's - Burgers & Wings (Leon Grill)",
4 | "cloudinaryImageId": "r4ufflqojich0r29efvc",
5 | "locality": "Koramangala",
6 | "areaName": "Koramangala",
7 | "costForTwo": "₹300 for two",
8 | "cuisines": ["American", "Snacks", "Turkish", "Portuguese", "Continental"],
9 | "avgRating": 4.3,
10 | "feeDetails": {
11 | "restaurantId": "65797",
12 | "fees": [
13 | {
14 | "name": "BASE_TIME"
15 | },
16 | {
17 | "name": "BASE_DISTANCE",
18 | "fee": 3300
19 | },
20 | {
21 | "name": "ANCILLARY_SURGE_FEE"
22 | }
23 | ],
24 | "totalFee": 3300
25 | },
26 | "parentId": "371281",
27 | "avgRatingString": "4.3",
28 | "totalRatingsString": "10K+",
29 | "sla": {
30 | "deliveryTime": 19,
31 | "lastMileTravel": 1.3,
32 | "serviceability": "SERVICEABLE",
33 | "slaString": "19 mins",
34 | "lastMileTravelString": "1.3 km",
35 | "iconType": "ICON_TYPE_EMPTY"
36 | },
37 | "availability": {
38 | "nextCloseTime": "2023-07-30 04:00:00",
39 | "opened": true
40 | },
41 | "badges": {},
42 | "isOpen": true,
43 | "type": "F",
44 | "badgesV2": {
45 | "entityBadges": {
46 | "imageBased": {},
47 | "textBased": {},
48 | "textExtendedBadges": {}
49 | }
50 | },
51 | "aggregatedDiscountInfoV3": {
52 | "header": "₹125 OFF",
53 | "subHeader": "ABOVE ₹249",
54 | "discountTag": "FLAT DEAL"
55 | },
56 | "orderabilityCommunication": {
57 | "title": {},
58 | "subTitle": {},
59 | "message": {},
60 | "customIcon": {}
61 | },
62 | "differentiatedUi": {
63 | "displayType": "ADS_UI_DISPLAY_TYPE_ENUM_DEFAULT",
64 | "differentiatedUiMediaDetails": {
65 | "mediaType": "ADS_MEDIA_ENUM_IMAGE",
66 | "lottie": {},
67 | "video": {}
68 | }
69 | },
70 | "reviewsSummary": {},
71 | "displayType": "RESTAURANT_DISPLAY_TYPE_DEFAULT",
72 | "restaurantOfferPresentationInfo": {}
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/sum.js:
--------------------------------------------------------------------------------
1 | export const sum = (a, b) => {
2 | return a + b;
3 | };
4 |
--------------------------------------------------------------------------------
/src/utils/UserContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | const UserContext = createContext({
4 | loggedInUser: "Default User",
5 | });
6 |
7 | export default UserContext;
8 |
--------------------------------------------------------------------------------
/src/utils/appStore.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import cartReducer from "./cartSlice";
3 |
4 | const appStore = configureStore({
5 | reducer: {
6 | cart: cartReducer,
7 | },
8 | });
9 |
10 | export default appStore;
11 |
--------------------------------------------------------------------------------
/src/utils/cartSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, current } from "@reduxjs/toolkit";
2 |
3 | const cartSlice = createSlice({
4 | name: "cart",
5 | initialState: {
6 | items: [],
7 | },
8 | reducers: {
9 | addItem: (state, action) => {
10 | // Redux Toolkit uses immer BTS
11 | state.items.push(action.payload);
12 | },
13 | removeItem: (state, action) => {
14 | state.items.pop();
15 | },
16 | //originalState = {items: ["pizza"]}
17 | clearCart: (state, action) => {
18 | //RTK - either Mutate the existing state or return a new State
19 | // state.items.length = 0; // originalState = []
20 |
21 | return { items: [] }; // this new object will be replaced inside originalState = { items: [] }
22 | },
23 | },
24 | });
25 |
26 | export const { addItem, removeItem, clearCart } = cartSlice.actions;
27 |
28 | export default cartSlice.reducer;
29 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const CDN_URL =
2 | "https://res.cloudinary.com/swiggy/image/upload/fl_lossy,f_auto,q_auto,w_508,h_320,c_fill/";
3 |
4 | export const LOGO_URL =
5 | "https://www.logodesign.net/logo/smoking-burger-with-lettuce-3624ld.png";
6 |
7 | export const MENU_API =
8 | "https://www.swiggy.com/dapi/menu/pl?page-type=REGULAR_MENU&complete-menu=true&lat=12.9351929&lng=77.62448069999999&restaurantId=";
9 |
--------------------------------------------------------------------------------
/src/utils/useOnlineStatus.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const useOnlineStatus = () => {
4 | const [onlineStatus, setOnlineStatus] = useState(true);
5 |
6 | useEffect(() => {
7 | window.addEventListener("offline", () => {
8 | setOnlineStatus(false);
9 | });
10 |
11 | window.addEventListener("online", () => {
12 | setOnlineStatus(true);
13 | });
14 | }, []);
15 |
16 | // boolean value
17 | return onlineStatus;
18 | };
19 |
20 | export default useOnlineStatus;
21 |
--------------------------------------------------------------------------------
/src/utils/useRestrauntMenu.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { MENU_API } from "../utils/constants";
3 |
4 | const useRestaurantMenu = (resId) => {
5 | const [resInfo, setResInfo] = useState(null);
6 |
7 | useEffect(() => {
8 | fetchData();
9 | }, []);
10 |
11 | const fetchData = async () => {
12 | const data = await fetch(MENU_API + resId);
13 | const json = await data.json();
14 | setResInfo(json.data);
15 | };
16 |
17 | return resInfo;
18 | };
19 |
20 | export default useRestaurantMenu;
21 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./src/**/*.{html,js}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------