Here is how to create an E-commerce site using React.js, Vite.js and TailwindCSS. The course name is: “Curso de React.js con Vite.js y TailwindCSS”.
Description 💡
Build an Online Store with React.js, the most in-demand tool for frontend developers. It integrates Vite.js for the development environment, TailwindCSS for style management and React Router DOM for routes and navigation. Transform Figma prototypes into professional web applications and deploy them to production with your Teffcode teacher.
- Build components to view products, purchase orders and filter by category
- Implement client-side navigation with React Router. Server-Side rendering (SSR)
- Consume the Platzi Fake Store using React.js.
Demos: JP·Shop | Shopi
Built With 🔑
This project is based on React.js con Vite.js y TailwindCSS
Content
1. Create a React Project using Vite
I. Go to Vite.js and click on Get Started
II. Run command:
1
npm create vite@latest ./
III. Configure the Vite Project
You will be asked a few questions:
1
2
3
4
5
Need to install the following packages:
create-vite@4.4.1
Ok to proceed? (y) no / `yes`
Select a Framework: › `React`
Select a variant? › `Javascript`
IV. Run command:
1
2
npm install
npm run dev
2. SetUp by Customizing the Project
This is the Starting Point Of The Project, where we will…
I. Delete unnecesary files
- Delete the
assets
folder and create a newassets
folder. - Delete the
public
folder and create a newpublic
folder. - Delete the content from
src/App.css
(keep the file) - Delete the content from
src/index.css
(keep the file) - Delete the content from
src/App.jsx
(keep the file) - Add the code below to
src/App.jsx
:1 2 3 4 5 6 7 8 9
import "./App.css"; function App() { return ( <> <div>Hello World</div> </> ); } export default App;
II. Install Tailwind in the project with Vite.
-
Run commands:
1 2
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p
III. Follow the Tailwind documentation for Vite.
-
Modify the
tailwind.config
file1 2 3 4 5 6 7 8
/** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], };
-
Add the Tailwind directives to your CSS
1 2 3
@tailwind base; @tailwind components; @tailwind utilities;
-
Modify the code below to
src/App.jsx
:1 2 3 4 5 6 7 8 9
import "./App.css"; function App() { return ( <> <div classname="bg-gray-500">Hello World</div> </> ); } export default App;
-
Run the project
1
npm run dev
IV. App Plugings
A. Install React Icons
1
npm i react-icons
In this proyect. I will use Heroicons
3. Create The Pages and Routes
For the Structure:
- Create a
Pages
folder inside thesrc
folder -
Inside
src/Pages
, create files: -Home/index.jsx
-MyAccount/index.jsx
-MyOrder/index.jsx
-MyOrders/index.jsx
-NotFound/index.jsx
-SignIn/index.jsx
Run
rafce
, deleteimport React from "react";
and ensure the routing works and the app is not broken. (modify themain.jsx
&App.jsx
) - Move and rename the
App.jsx
file » tosrc/Pages/App/index.jsx
-
Modify the
src/Pages/App/index.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
import "./App.css"; import Home from "../Home"; import MyAccount from "../MyAccount"; import MyOrder from "../MyOrder"; import MyOrders from "../MyOrders"; import NotFound from "../NotFound"; import SignIn from "../SignIn"; function App() { return ( <> <div className="flex flex-col"> <Home /> <MyAccount /> <MyOrder /> <MyOrders /> <NotFound /> <SignIn /> </div> </> ); } export default App;
-
Modify the
main.jsx
1 2 3 4 5 6 7 8 9 10
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./Pages/App/index.jsx"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <App /> </React.StrictMode> );
4. Routing
Install React Router DOM
1
npm i react-router-dom
I. Edit the App/index.jsx
file:
Located in src/Pages/App/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useRoutes, BrowserRouter } from "react-router-dom";
import Home from "../Home";
import MyAccount from "../MyAccount";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import SignIn from "../SignIn";
import "./App.css";
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/my-account", element: <MyAccount /> },
{ path: "/my-order", element: <MyOrder /> },
{ path: "/my-orders", element: <MyOrders /> },
{ path: "/sign-in", element: <SignIn /> },
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
);
};
export default App;
5. Navbar Component
I. Create the Navbar
Component
Located in src/Components/Navbar/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import { NavLink } from "react-router-dom";
const Navbar = () => {
const activeStyle = "underline text-gray-500 underline-offset-4";
return (
<nav className="flex justify-between items-center fixed z-10 w-full py-5 px-8 text-md font-light top-0">
<ul className="flex items-center gap-3">
<li className="font-semibold text-lg">
<NavLink to="/">Shopi</NavLink>
</li>
<li>
<NavLink
to="/"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
All
</NavLink>
</li>
<li>
<NavLink
to="/clothes"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Clothes
</NavLink>
</li>
<li>
<NavLink
to="/electronics"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Electronics
</NavLink>
</li>
<li>
<NavLink
to="/furnitures"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Furnitures
</NavLink>
</li>
<li>
<NavLink
to="/toys"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Toys
</NavLink>
</li>
</ul>
<ul className="flex items-center gap-3">
<li>
<NavLink
to="/my-orders"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
My Orders
</NavLink>
</li>
<li>
<NavLink
to="/my-account"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
My Account
</NavLink>
</li>
<li>
<NavLink
to="/sign-in"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Sign In
</NavLink>
</li>
<li>
<NavLink
to="/card"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
🛒 0
</NavLink>
</li>
</ul>
</nav>
);
};
export default Navbar;
II. Modify the App
file
Located in src/Pages/App/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { useRoutes, BrowserRouter } from "react-router-dom";
import Home from "../Home";
import MyAccount from "../MyAccount";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import SignIn from "../SignIn";
import Navbar from "../../Components/Navbar";
import "./App.css";
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/my-account", element: <MyAccount /> },
{ path: "/my-order", element: <MyOrder /> },
{ path: "/my-orders", element: <MyOrders /> },
{ path: "/sign-in", element: <SignIn /> },
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<BrowserRouter>
<AppRoutes />
<Navbar />
</BrowserRouter>
);
};
export default App;
6. Layout Componente
I. Create the Layout
Component
Located in src/Components/Layout/index.jsx
1
2
3
4
5
const Layout = ({ children }) => {
return <div className="flex flex-col items-center mt-20">{children}</div>;
};
export default Layout;
II. Add the Layout to all my pages:
Home/index.jsx
MyAccount/index.jsx
MyOrder/index.jsx
MyOrders/index.jsx
NotFound/index.jsx
SignIn/index.jsx
Include the following structure:
1
2
3
4
5
6
7
import Layout from "../../Components/Layout";
const NameHere = () => {
return <Layout>NameHere</Layout>;
};
export default NameHere;
Replace the
NameHere
with the name of the folder.
7. Card Componente
I. Create the Card
Component
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const Card = () => {
return (
<div className="bg-amber-600/80 cursor-pointer w-56 h-60 rounded-lg">
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
tag
</span>
<img
className="rounded-lg w-full h-full object-cover"
src="https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300"
alt="card image"
/>
<div className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2">
{" "}
+{" "}
</div>
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">Headphones</span>
<span className="text-lg font-medium">$300</span>
</p>
</div>
);
};
export default Card;
II. Modify the Home
Component
Located in src/Components/Home/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
import Card from "../../Components/Card";
import Layout from "../../Components/Layout";
const Home = () => {
return (
<Layout>
<Card />
</Layout>
);
};
export default Home;
8. Consume an API
Consuming APIs is the process by which the application developer accesses the various APIs that are exposed by you
I. Go the API Website and read the Documentation
It is Extremely important to understand how to use and consume the API we are about to use.
API Examples:
II. Use the API
After finding what we needed to do to be able to use the API
- Get the API URL from the section/element that we want to use
Example: Fake Store API
1
2
3
4
// Get all products:
fetch("https://fakestoreapi.com/products")
.then((res) => res.json())
.then((json) => console.log(json));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Output:
[
{
id: 1,
title: "...",
price: "...",
category: "...",
description: "...",
image: "...",
},
/*...*/
{
id: 30,
title: "...",
price: "...",
category: "...",
description: "...",
image: "...",
},
];
Example: Fake Platzi API
1
2
3
4
// Get all products:
fetch("https://api.escuelajs.co/api/v1/products")
.then((res) => res.json())
.then((json) => console.log(json));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Output:
[
{
id: 4,
title: "Handmade Fresh Table",
price: 687,
description: "Andy shoes are designed to keeping in...",
category: {
id: 5,
name: "Others",
image: "https://placeimg.com/640/480/any?r=0.591926261873231",
},
images: [
"https://placeimg.com/640/480/any?r=0.9178516507833767",
"https://placeimg.com/640/480/any?r=0.9300320592588625",
"https://placeimg.com/640/480/any?r=0.8807778235430017",
],
},
// ...
];
State and Effect are use to consume an API
III. Modify the Home
Page
Located in src/Pages/Home/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState, useEffect } from "react"; // Hooks to consume the API
import Card from "../../Components/Card";
import Layout from "../../Components/Layout";
const Home = () => {
// UseState is a hook to add the info from the API to the state
const [items, setItems] = useState(null);
// UseEffect is a hook to fetch the data from the API
useEffect(() => {
fetch("https://api.escuelajs.co/api/v1/products")
.then((response) => response.json())
.then((json) => setItems(json));
}, []);
return (
<Layout>
<div className="grid gap-4 grid-cols-4 w-full max-w-screen-lg">
{items?.map((item) => (
<Card key={item.id} data={item} />
))}
</div>
</Layout>
);
};
export default Home;
IV. Modify the Card
Component
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Card = (data) => {
return (
<div className="bg-amber-700/40 cursor-pointer w-56 h-60 rounded-lg">
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{data.data.category.name}
</span>
<img
className="rounded-lg w-full h-full object-cover"
src={data.data.images}
alt={data.data.title}
/>
<div className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2">
+
</div>
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">{data.data.title}</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
9. Global Context (or use useContext instead)
Global state with the Context API
I. Create the Context file
Located in src/Context/index.jsx
1
2
3
4
5
6
7
import { createContext } from "react";
const AppContext = createContext();
export const AppProvider = ({ children }) => {
return <AppContext.Provider>{children}</AppContext.Provider>;
};
II. Modify the App
file
Located in src/Pages/App/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { useRoutes, BrowserRouter } from "react-router-dom";
import { AppProvider } from "../../Context";
import Home from "../Home";
import MyAccount from "../MyAccount";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import SignIn from "../SignIn";
import Navbar from "../../Components/Navbar";
// import TestNavbar from "../../Components/TestJp/Navbar.jsx";
import "./App.css";
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/my-account", element: <MyAccount /> },
{ path: "/my-order", element: <MyOrder /> },
{ path: "/my-orders", element: <MyOrders /> },
{ path: "/sign-in", element: <SignIn /> },
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<AppProvider>
<BrowserRouter>
<AppRoutes />
<Navbar />
</BrowserRouter>
</AppProvider>
);
};
export default App;
10. Cart Counter
I. Modify the Card
Component
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { useContext } from "react";
import { AppContext } from "../../Context";
const Card = (data) => {
const context = useContext(AppContext);
return (
<div className="bg-amber-700/40 cursor-pointer w-56 h-60 rounded-lg">
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{data.data.category.name}
</span>
<img
className="rounded-lg w-full h-full object-cover"
src={data.data.image} // This is for the Fake Store API
// src={data.data.images} // This is for the Platzi API (which is not very stable)
alt={data.data.title}
/>
<div
onClick={() => context.setCount(context.count + 1)}
className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2"
>
+
</div>
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">{data.data.title}</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
II. Modify the Navbar
Component
Located in src/Components/Navbar/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { NavLink } from "react-router-dom";
import { useContext } from "react";
import { AppContext } from "../../Context";
const Navbar = () => {
const activeStyle = "underline text-gray-500 underline-offset-4";
const context = useContext(AppContext);
return (
<nav className="flex justify-between items-center fixed z-10 w-full py-5 px-8 text-md font-light top-0">
<ul className="flex items-center gap-3">
<li className="font-semibold text-lg">
<NavLink to="/">Shopi</NavLink>
</li>
<li>
<NavLink
to="/"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
All
</NavLink>
</li>
<li>
<NavLink
to="/clothes"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Clothes
</NavLink>
</li>
<li>
<NavLink
to="/electronics"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Electronics
</NavLink>
</li>
<li>
<NavLink
to="/furnitures"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Furnitures
</NavLink>
</li>
<li>
<NavLink
to="/toys"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Toys
</NavLink>
</li>
</ul>
<ul className="flex items-center gap-3">
<li>
<NavLink
to="/my-orders"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
My Orders
</NavLink>
</li>
<li>
<NavLink
to="/my-account"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
My Account
</NavLink>
</li>
<li>
<NavLink
to="/sign-in"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Sign In
</NavLink>
</li>
<li>
<NavLink
to="/card"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
🛒 {context.count}
</NavLink>
</li>
</ul>
</nav>
);
};
export default Navbar;
II. Modify the Context
file
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createContext, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// UseState is a hook to add the info from the API to the state
const [count, setCount] = useState(0);
// To inspect the value of count: // console.log(count);
return (
<AppContext.Provider
value={
count,
setCount,
}
>
{children}
</AppContext.Provider>
);
};
value
is missing another set of{}
. Should look like this:value={ { ...content here.... } }
11. SideBar Component
I. Create the ProductDetail
Component
Located in src/Components/ProductDetail/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
const ProductDetail = () => {
return (
<aside className="flex flex-col fixed right-0 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2">
{/* top-[68px] w-[360px] h-[calc(100vh-68px)] */}
<div>
<h1>Product Detail</h1>
<img src="" alt="image" />
</div>
</aside>
);
};
export default ProductDetail;
II. Modify the Home
Page
Located in src/Pages/Home/index.jsx
- Add the
ProductDetail
Component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { useState, useEffect } from "react";
import Card from "../../Components/Card";
import Layout from "../../Components/Layout";
import ProductDetail from "../../Components/ProductDetail";
const Home = () => {
// UseState is a hook to add the info from the API to the state
const [items, setItems] = useState(null);
// UseEffect is a hook to fetch the data from the API
useEffect(() => {
// fetch("https://api.escuelajs.co/api/v1/products") // This is for the Platzi API (which is not very stable)
fetch("https://fakestoreapi.com/products") // This is for the Fake Store API
.then((response) => response.json())
.then((json) => setItems(json));
}, []);
return (
<Layout>
Home
<div className="grid gap-4 grid-cols-4 w-full max-w-screen-lg">
{items?.map((item) => (
<Card key={item.id} data={item} />
))}
</div>
<ProductDetail />
</Layout>
);
};
export default Home;
12. Add React Icon to the Proyect
React Icon needs to be install. (Docs).
I. Modify the ProductDetail
Component
Located in src/Components/ProductDetail/index.jsx
Add the Exit icon (x)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { HiOutlineX } from "react-icons/hi";
const ProductDetail = () => {
return (
<aside className="flex flex-col fixed right-0 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2">
{/* top-[68px] w-[360px] h-[calc(100vh-68px)] */}
<div className="flex justify-between items-center ">
<h2 className="font-medium">Product Detail</h2>
<HiOutlineX />
</div>
</aside>
);
};
export default ProductDetail;
II. Modify the Card
Component
Located in src/Components/Card/index.jsx
Add the Plus icon (+)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiPlusSm } from "react-icons/hi";
const Card = (data) => {
const context = useContext(AppContext);
return (
<div className="bg-amber-700/40 cursor-pointer w-56 h-60 rounded-lg">
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{/* {data.data.category.name} is for the Platzi API (which is not very stable) */}
{/* {data.data.category.name} */}
{/* {data.data.category} is for the Fake Store API */}
{data.data.category}
</span> </span>
<img
className="rounded-lg w-full h-full object-cover"
src={data.data.image} // This is for the Fake Store API
// src={data.data.images} // This is for the Platzi API (which is not very stable)
alt={data.data.title}
/>
<HiPlusSm
onClick={() => context.setCount(context.count + 1)}
className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2"
/>
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">{data.data.title}</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
III. Modify the Navbar
Component
Located in src/Components/Navbar/index.jsx
Add the Shopping Cart icon (🛒)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import { NavLink } from "react-router-dom";
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiOutlineShoppingCart } from "react-icons/hi";
const Navbar = () => {
const activeStyle = "underline text-gray-500 underline-offset-4";
const context = useContext(AppContext);
return (
<nav className="flex justify-between items-center fixed z-10 w-full py-5 px-8 text-md font-light top-0">
<ul className="flex items-center gap-3">
<li className="font-semibold text-lg">
<NavLink to="/">Shopi</NavLink>
</li>
<li>
<NavLink
to="/"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
All
</NavLink>
</li>
<li>
<NavLink
to="/clothes"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Clothes
</NavLink>
</li>
<li>
<NavLink
to="/electronics"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Electronics
</NavLink>
</li>
<li>
<NavLink
to="/furnitures"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Furnitures
</NavLink>
</li>
<li>
<NavLink
to="/toys"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Toys
</NavLink>
</li>
</ul>
<ul className="flex items-center gap-3">
<li>
<NavLink
to="/my-orders"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
My Orders
</NavLink>
</li>
<li>
<NavLink
to="/my-account"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
My Account
</NavLink>
</li>
<li>
<NavLink
to="/sign-in"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Sign In
</NavLink>
</li>
<li>
<NavLink
to="/card"
className={`flex justify-center items-center ${({ isActive }) =>
isActive ? activeStyle : undefined}`}
>
<HiOutlineShoppingCart className="mr-1" /> {context.count}
</NavLink>
</li>
</ul>
</nav>
);
};
export default Navbar;
13. Functions to Open And Close the Product Detail
I. Modify the Context
file
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { createContext, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Hook to add the info from the API to the state:
const [count, setCount] = useState(0);
// To inspect the value of count: // console.log(count);
// Hook to open and close the product detail component:
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
return (
<AppContext.Provider
value={
count,
setCount,
openProductDetail,
closeProductDetail,
isProductDetailOpen,
}
>
{children}
</AppContext.Provider>
);
};
value
is missing another set of{}
. It should look like this:value={ { ...content here.... } }
II. Modify the Product Detail
Component
Located in src/Components/ProductDetail/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { HiOutlineX } from "react-icons/hi";
import { useContext } from "react";
import { AppContext } from "../../Context";
const ProductDetail = () => {
const context = useContext(AppContext);
return (
<aside
className={`${
context.isProductDetailOpen ? "flex" : "hidden"
} flex-col fixed right-0 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2`}
>
{/* top-[68px] w-[360px] h-[calc(100vh-68px)] */}
<div className="flex justify-between items-center ">
<h2 className="font-medium">Product Detail</h2>
<HiOutlineX onClick={() => context.closeProductDetail()} />
</div>
</aside>
);
};
export default ProductDetail;
III. Modify the Card
Component
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiPlusSm } from "react-icons/hi";
const Card = (data) => {
const context = useContext(AppContext);
return (
<div
className="bg-amber-700/40 cursor-pointer w-56 h-60 rounded-lg"
onClick={() => context.openProductDetail()}
>
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{/* {data.data.category.name} is for the Platzi API (which is not very stable) */}
{/* {data.data.category.name} */}
{/* {data.data.category} is for the Fake Store API */}
{data.data.category}
</span>
<img
className="rounded-lg w-full h-full object-cover"
src={data.data.image} // This is for the Fake Store API
// src={data.data.images} // This is for the Platzi API (which is not very stable)
alt={data.data.title}
/>
<HiPlusSm
onClick={() => context.setCount(context.count + 1)}
className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2"
/>
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">{data.data.title}</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
14. Show the Products on ProductDetail
I. Modify the Context
file
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { createContext, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Shopping Cart · Increment quantity
const [count, setCount] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
return (
<AppContext.Provider
value={
count,
setCount,
openProductDetail,
closeProductDetail,
isProductDetailOpen,
productToShow,
setProductToShow,
}
>
{children}
</AppContext.Provider>
);
};
II. Modify the Product Detail
Component
Located in src/Components/ProductDetail/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { HiOutlineX } from "react-icons/hi";
import { useContext } from "react";
import { AppContext } from "../../Context";
const ProductDetail = () => {
const context = useContext(AppContext);
return (
<aside
className={`${
context.isProductDetailOpen ? "flex" : "hidden"
} flex-col fixed right-0 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2`}
>
{/* top-[68px] w-[360px] h-[calc(100vh-68px)] */}
<div className="flex justify-between items-center ">
<h2 className="font-medium">Product Detail</h2>
<HiOutlineX onClick={() => context.closeProductDetail()} />
</div>
<figure className="px-6">
<img
className="w-full h-full rounded-lg"
src={context.productToShow.image}
alt={context.productToShow.title}
/>
</figure>
<p className="flex flex-col p-6">
<span className="font-medium text-2xl mb-2">
${context.productToShow.price}
</span>
<span className="font-medium text-md">
${context.productToShow.title}
</span>
<span className="font-light text-sm">
${context.productToShow.description}
</span>
</p>
</aside>
);
};
export default ProductDetail;
III. Modify the Card
Component
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiPlusSm } from "react-icons/hi";
const Card = (data) => {
const context = useContext(AppContext);
const showProduct = (productDetail) => {
context.openProductDetail();
context.setProductToShow(productDetail);
};
return (
<div
className="bg-amber-700/40 cursor-pointer w-56 h-60 rounded-lg"
onClick={() => showProduct(data.data)}
>
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{data.data.category}
</span>
<img
className="rounded-lg w-full h-full object-cover"
src={data.data.image}
alt={data.data.title}
/>
<HiPlusSm
onClick={() => context.setCount(context.count + 1)}
className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2"
/>
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">{data.data.title}</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
15. Add Products to My Order
I. Modify the Context
file
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { createContext, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Shopping Cart · Increment quantity
const [count, setCount] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
// Shopping Cart · add product to cart
const [cartProducts, setCartProducts] = useState([]);
return (
<AppContext.Provider
value={
count,
setCount,
openProductDetail,
closeProductDetail,
isProductDetailOpen,
productToShow,
setProductToShow,
cartProducts,
setCartProducts,
}
>
{children}
</AppContext.Provider>
);
};
value
is missing another set of{}
. It should look like this:value={ { ...content here.... } }
II. Modify the Card
Component
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiPlusSm } from "react-icons/hi";
const Card = (data) => {
const context = useContext(AppContext);
const showProduct = (productDetail) => {
context.openProductDetail();
context.setProductToShow(productDetail);
};
const addProductToCart = (productData) => {
context.setCount(context.count + 1);
context.setCartProducts([...context.cartProducts, productData]);
console.log(context.cartProducts);
};
return (
<div
className="bg-amber-700/40 cursor-pointer w-56 h-60 rounded-lg"
onClick={() => showProduct(data.data)}
>
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{data.data.category}
</span>
<img
className="rounded-lg w-full h-full object-cover"
src={data.data.image}
alt={data.data.title}
/>
<HiPlusSm
onClick={() => addProductToCart(data.data)}
className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2"
/>
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">{data.data.title}</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
16. Add a BackUp API
Include a Backup API in case the current API is down
During the building of this proyect, the Fake APi I was using went down. I had to use another one to continuouse with the course.
I. Modify the Home Page
Located in src/Pages/Home/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { useState, useEffect } from "react";
import Card from "../../Components/Card";
import Layout from "../../Components/Layout";
import ProductDetail from "../../Components/ProductDetail";
import CheckoutSideMenu from "../../Components/CheckoutSideMenu";
const Home = () => {
// UseState is a hook to add the info from the API to the state
const [items, setItems] = useState(null);
// UseEffect is a hook to fetch the data from the API
useEffect(() => {
// fetch("https://fakestoreapi.com/products") // Fake Store API
fetch("https://api.escuelajs.co/api/v1/products") // Platzi API
.then((response) => response.json())
.then((json) => setItems(json));
}, []);
return (
<Layout>
Home
<div className="grid gap-4 grid-cols-4 w-full max-w-screen-lg">
{items?.map((item) => (
<Card key={item.id} data={item} />
))}
</div>
<ProductDetail />
<CheckoutSideMenu />
</Layout>
);
};
export default Home;
II. Modify the Card Components
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiPlusSm } from "react-icons/hi";
const Card = (data) => {
const context = useContext(AppContext);
const showProduct = (productDetail) => {
context.openProductDetail();
context.setProductToShow(productDetail);
};
const addProductToCart = (productData) => {
context.setCount(context.count + 1);
context.setCartProducts([...context.cartProducts, productData]);
console.log(context.cartProducts);
};
return (
<div
className="bg-amber-700/40 cursor-pointer w-56 h-60 rounded-lg"
onClick={() => showProduct(data.data)}
>
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{/* Fake Store API: */}
{/* {data.data.category} */}
{/* Platzi API: */}
{data.data.category.name}
</span>
<img
className="rounded-lg w-full h-full object-cover"
// src={data.data.image} // Fake Store API
src={data.data.images} // Platzi API
alt={data.data.title} // Fake Store API
/>
<HiPlusSm
onClick={() => addProductToCart(data.data)}
className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2"
/>
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">{data.data.title}</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
III. Modify the ProductDetail Components
Located in src/Components/ProductDetail/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { HiOutlineX } from "react-icons/hi";
import { useContext } from "react";
import { AppContext } from "../../Context";
const ProductDetail = () => {
const context = useContext(AppContext);
return (
<aside
className={`${
context.isProductDetailOpen ? "flex" : "hidden"
} flex-col fixed right-0 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2`}
>
{/* top-[68px] w-[360px] h-[calc(100vh-68px)] */}
<div className="flex justify-between items-center ">
<h2 className="font-medium">Product Detail</h2>
<HiOutlineX onClick={() => context.closeProductDetail()} />
</div>
<figure className="px-6">
<img
className="w-full h-full rounded-lg"
alt={context.productToShow.title}
// Platzi API:
src={context.productToShow.images}
// Fake Store API:
// src={context.productToShow.image}
/>
</figure>
<p className="flex flex-col p-6">
<span className="font-medium text-2xl mb-2">
${context.productToShow.price}
</span>
<span className="font-medium text-md">
${context.productToShow.title}
</span>
<span className="font-light text-sm">
{context.productToShow.description}
</span>
</p>
</aside>
);
};
export default ProductDetail;
17. Create the Side Menu for the Shopping cart
I. Create a CheckoutSideMenu
Component
Located in src/Components/CheckoutSideMenu/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useContext } from "react";
import { HiOutlineX } from "react-icons/hi";
import { AppContext } from "../../Context";
const CheckoutSideMenu = () => {
const context = useContext(AppContext);
return (
<aside
className={`${
context.isCheckoutSideMenuOpen ? "flex" : "hidden"
} flex-col fixed right-0 top-20 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2`}
>
<div className="flex justify-between items-center ">
<h2 className="font-medium">My Order</h2>
<div>
<HiOutlineX onClick={() => context.closeCheckoutSideMenu()} />
</div>
</div>
</aside>
);
};
export default CheckoutSideMenu;
II. Modify the Context
file
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { createContext, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Shopping Cart · Increment quantity
const [count, setCount] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
// Shopping Cart · add product to cart
const [cartProducts, setCartProducts] = useState([]);
// Checkout Side Menu · Open/Close
const [isCheckoutSideMenuOpen, setIsCheckoutSideMenuOpen] = useState(false);
const openCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(true);
const closeCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(false);
return (
<AppContext.Provider
value={
count,
setCount,
openProductDetail,
closeProductDetail,
isProductDetailOpen,
productToShow,
setProductToShow,
cartProducts,
setCartProducts,
isCheckoutSideMenuOpen,
openCheckoutSideMenu,
closeCheckoutSideMenu,
}
>
{children}
</AppContext.Provider>
);
};
value
is missing another set of{}
. It should look like this:value={ { ...content here.... } }
III. Modify the Card Components
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiPlusSm } from "react-icons/hi";
const Card = (data) => {
const context = useContext(AppContext);
const showProduct = (productDetail) => {
context.openProductDetail();
context.setProductToShow(productDetail);
context.closeCheckoutSideMenu();
};
const addProductToCart = (event, productData) => {
event.stopPropagation(); // Para que no se abra el modal de detalle de producto
context.setCount(context.count + 1);
context.setCartProducts([...context.cartProducts, productData]);
// console.log(context.cartProducts);
context.openCheckoutSideMenu();
context.closeProductDetail();
};
return (
<div
className="bg-amber-700/40 cursor-pointer w-56 h-60 rounded-lg"
onClick={() => showProduct(data.data)}
>
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{/* Fake Store API: */}
{/* {data.data.category} */}
{/* Platzi API: */}
{data.data.category.name}
</span>
<img
className="rounded-lg w-full h-full object-cover"
// src={data.data.image} // Fake Store API
src={data.data.images} // Platzi API
alt={data.data.title} // Fake Store API
/>
<HiPlusSm
onClick={(event) => addProductToCart(event, data.data)}
className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2"
/>
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">{data.data.title}</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
IV. Modify the App
file
Located in src/Pages/App/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { useRoutes, BrowserRouter } from "react-router-dom";
import { AppProvider } from "../../Context";
import Home from "../Home";
import MyAccount from "../MyAccount";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import SignIn from "../SignIn";
import Navbar from "../../Components/Navbar";
// import TestNavbar from "../../Components/TestJp/Navbar.jsx";
import "./App.css";
import CheckoutSideMenu from "../../Components/CheckoutSideMenu";
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/my-account", element: <MyAccount /> },
{ path: "/my-order", element: <MyOrder /> },
{ path: "/my-orders", element: <MyOrders /> },
{ path: "/sign-in", element: <SignIn /> },
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<AppProvider>
<BrowserRouter>
<AppRoutes />
<Navbar />
<CheckoutSideMenu />
</BrowserRouter>
</AppProvider>
);
};
export default App;
18. Order Card Component
I. Create the OrderCard
Component
Located in src/Components/OrderCard/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { HiOutlineX } from "react-icons/hi";
import { AppContext } from "../../Context";
import { useContext } from "react";
const OrderCard = (props) => {
const { title, imageUrl, price } = props;
const context = useContext(AppContext);
return (
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<figure className="w-20 h-20 m-0.5">
<img
className="w-full h-full rounded-lg object-cover"
src={imageUrl}
alt={title}
/>
</figure>
<p className="text-sm font-light">{title}</p>
</div>
<div className="flex items-center gap-2">
<p className="text-lg font-medium">$ {price}</p>
<>
<HiOutlineX
onClick={() => context.closeCheckoutSideMenu()}
className="h-6 w-6 text-black cursor-pointer"
/>
</>
</div>
</div>
);
};
export default OrderCard;
II. Modify the CheckoutSideMenu
Component
Located in src/Components/CheckoutSideMenu/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { useContext } from "react";
import { HiOutlineX } from "react-icons/hi";
import { AppContext } from "../../Context";
import OrderCard from "../OrderCard";
const CheckoutSideMenu = () => {
const context = useContext(AppContext);
return (
<aside
className={`${
context.isCheckoutSideMenuOpen ? "flex" : "hidden"
} flex-col fixed right-0 top-20 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2`}
>
<div className="flex justify-between items-center p-6">
<h2 className="font-medium">My Order</h2>
<div>
<HiOutlineX onClick={() => context.closeCheckoutSideMenu()} />
</div>
</div>
<div className="px-6">
{context.cartProducts.map((product) => (
<OrderCard
key={product.id}
title={product.title}
// imageUrl={product.image} // Fake Store API
imageUrl={product.images} // Platzi API
price={product.price}
/>
))}
</div>
</aside>
);
};
export default CheckoutSideMenu;
19. Don’t Repeat Products in Cart
I. Modify the Card Components
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiCheck, HiPlusSm } from "react-icons/hi";
const Card = (data) => {
const context = useContext(AppContext);
const showProduct = (productDetail) => {
context.openProductDetail();
context.setProductToShow(productDetail);
context.closeCheckoutSideMenu();
};
const addProductToCart = (event, productData) => {
event.stopPropagation(); // Para que no se abra el modal de detalle de producto
context.setCount(context.count + 1);
context.setCartProducts([...context.cartProducts, productData]);
// console.log(context.cartProducts);
context.openCheckoutSideMenu();
context.closeProductDetail();
};
// Check if the product is in the cart:
const renderIcon = (id) => {
const productIsInCart =
context.cartProducts.filter((product) => product.id === id).length > 0;
if (productIsInCart) {
return (
<HiCheck className="absolute top-0 right-0 flex justify-center items-center bg-black rounded-full w-6 h-6 m-2 text-white" />
);
} else {
return (
<HiPlusSm
onClick={(event) => addProductToCart(event, data.data)}
className="absolute top-0 right-0 flex justify-center items-center bg-white rounded-full w-6 h-6 m-2"
/>
);
}
};
return (
<div
className="bg-amber-700/40 cursor-pointer w-56 h-60 rounded-lg"
onClick={() => showProduct(data.data)}
>
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{/* Fake Store API: */}
{/* {data.data.category} */}
{/* Platzi API: */}
{data.data.category.name}
</span>
<img
className="rounded-lg w-full h-full object-cover"
// src={data.data.image} // Fake Store API
src={data.data.images} // Platzi API
alt={data.data.title} // Fake Store API
/>
{renderIcon(data.data.id)}
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">{data.data.title}</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
20. To Modify the Quantity of the Products on my Order
In beta for now. I didnt actually add it to the project.
Modify the OrderCard
Component
Located in src/Components/OrderCard/index.jsx
- Increase & decrease the quantity of items.
- Update the price depending on the quantity of items.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { HiOutlineX } from "react-icons/hi";
import { AppContext } from "../../Context";
import { useContext, useState } from "react";
const OrderCard = (props) => {
const { title, imageUrl, price } = props;
const context = useContext(AppContext);
// quantity of the item in the cart:
const [quantity, setQuantity] = useState(1);
const incrementQuantity = () => {
setQuantity(quantity + 1);
};
const decrementQuantity = () => {
if (quantity > 1) {
setQuantity(quantity - 1);
}
};
return (
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<figure className="w-20 h-20 m-0.5">
<img
className="w-full h-full rounded-lg object-cover"
src={imageUrl}
alt={title}
/>
</figure>
<p className="text-sm font-light">{title}</p>
</div>
<div className="flex items-center gap-2">
<p className="text-lg font-medium">${price * quantity}</p>
<button onClick={decrementQuantity}>-</button>
<p>{quantity} item</p>
<button onClick={incrementQuantity}>+</button>
<>
<HiOutlineX
onClick={() => context.closeCheckoutSideMenu()}
className="h-6 w-6 text-black cursor-pointer"
/>
</>
</div>
</div>
);
};
export default OrderCard;
I asked Copilote with this and it was pretty simple.
21. Use Components from Shadcn-Ui
In beta for now. I didnt actually add it to the project.
SetUp Shadcn UI in the Project
I. Install Shadcn UI for Vite.
Shadcn UI. is a library with multiple designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source. it will be use to style the page.
- Create a
jsconfig.json
file by adding the following code to the paths section:
1
2
3
4
5
6
7
8
9
10
11
12
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
// ...
}
}
- Install Node.
1
npm i -D @types/node
-
Overwrite the
vite.config.ts
file:1 2 3 4 5 6 7 8 9 10 11 12
import path from "path" import react from "@vitejs/plugin-react" import { defineConfig } from "vite" export default defineConfig({ plugins: [react()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, })
- Run the shadcn-ui init command to setup your project:
1
npx shadcn-ui@latest init
- Configure
components.json
You will be asked a few questions to configure components.json
:
1
2
3
4
5
6
7
8
9
10
Would you like to use TypeScript (recommended)? `no` / yes ... no
Which style would you like to use? › `Default`
Which color would you like to use as base color? › `Slate`
Where is your global CSS file? › › `src/Pages/App/App.css`
Do you want to use CSS variables for colors? › `no` / yes ... no
Where is your tailwind.config.js located? › `tailwind.config.js`
Configure the import alias for components: › `@/components`
Configure the import alias for utils: › `@/lib/utils`
Are you using React Server Components? › no / `yes` ... yes
√ Write configuration to components.json. Proceed? ... yes
This will install all the necesary components
II. Add a Dropdown Menu Component from shadcn-UI
- Install component
1
npx shadcn-ui@latest add dropdown-menu
- Modify the
Order
22. Delete an Item From the Order
I. Modify the CheckoutSideMenu
Component
Located in src/Components/CheckoutSideMenu/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { useContext } from "react";
import { HiOutlineX } from "react-icons/hi";
import { AppContext } from "../../Context";
import OrderCard from "../OrderCard";
const CheckoutSideMenu = () => {
const context = useContext(AppContext);
const handleDeleteProduct = (id) => {
const newCartProducts = context.cartProducts.filter(
(product) => product.id !== id
);
context.setCartProducts(newCartProducts);
context.setCart(context.cart - 1);
};
return (
<aside
className={`${
context.isCheckoutSideMenuOpen ? "flex" : "hidden"
} flex-col fixed right-0 top-20 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2`}
>
<div className="flex justify-between items-center p-6">
<h2 className="font-medium">My Order</h2>
<div>
<HiOutlineX onClick={() => context.closeCheckoutSideMenu()} />
</div>
</div>
<div className="px-6 overflow-y-scroll">
{context.cartProducts.map((product) => (
<OrderCard
key={product.id}
id={product.id}
title={product.title}
// imageUrl={product.image} // Fake Store API
imageUrl={product.images} // Platzi API
price={product.price}
quantity={product.quantity}
handleDeleteProduct={handleDeleteProduct}
/>
))}
</div>
</aside>
);
};
export default CheckoutSideMenu;
II. Modify the OrderCard
Component
Located in src/Components/OrderCard/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { HiOutlineTrash } from "react-icons/hi";
import { AppContext } from "../../Context";
import { useContext } from "react";
const OrderCard = (props) => {
const { id, title, imageUrl, price, handleDeleteProduct } = props;
return (
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<figure className="w-20 h-20 m-0.5">
<img
className="w-full h-full rounded-lg object-cover"
src={imageUrl}
alt={title}
/>
</figure>
{/* <p className="text-sm font-light">{title}</p> */}
</div>
<div className="flex items-center gap-2">
<p className="text-lg font-medium">${price}</p>
<>
<HiOutlineTrash
onClick={() => handleDeleteProduct(id)}
className="h-6 w-6 text-black cursor-pointer"
/>
</>
</div>
</div>
);
};
export default OrderCard;
23. Total Items and Total Price of the Order
I. Create a Utils
file
Located in src/Utils/index.js
1
2
3
4
5
6
7
8
9
10
11
// **
// * @description: This function calculates the total price of the new order
// * @param {Array} products
// * @return: {Number} total price
// **
export const totalPrice = (products) => {
let sum = 0;
products.forEach((product) => (sum += product.price));
return sum;
};
II. Modify the CheckoutSideMenu
Component
Located in src/Components/CheckoutSideMenu/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { useContext } from "react";
import { HiOutlineX } from "react-icons/hi";
import { AppContext } from "../../Context";
import OrderCard from "../OrderCard";
import { totalPrice } from "../../Utils/index.js";
const CheckoutSideMenu = () => {
const context = useContext(AppContext);
const handleDeleteProduct = (id) => {
const newCartProducts = context.cartProducts.filter(
(product) => product.id !== id
);
context.setCartProducts(newCartProducts);
context.setCart(context.cart - 1);
};
return (
<aside
className={`${
context.isCheckoutSideMenuOpen ? "flex" : "hidden"
} flex-col fixed right-0 top-20 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2`}
>
<div className="flex justify-between items-center p-6">
<h2 className="font-medium">My Order</h2>
<div>
<HiOutlineX onClick={() => context.closeCheckoutSideMenu()} />
</div>
</div>
<div className="px-6 overflow-y-scroll">
{context.cartProducts.map((product) => (
<OrderCard
key={product.id}
id={product.id}
title={product.title}
// imageUrl={product.image} // Fake Store API
imageUrl={product.images} // Platzi API
price={product.price}
quantity={product.quantity}
handleDeleteProduct={handleDeleteProduct}
/>
))}
</div>
<div className="p-6">
<p>
<span className="font-medium">Total</span>
<span className="font-medium">
${totalPrice(context.cartProducts)}
</span>
</p>
</div>
</aside>
);
};
export default CheckoutSideMenu;
24. Create a New Order
I. Modify the Context
file
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { createContext, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Shopping Cart · Increment quantity
const [cart, setCart] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
// Shopping Cart · add product to cart
const [cartProducts, setCartProducts] = useState([]);
// Checkout Side Menu · Open/Close
const [isCheckoutSideMenuOpen, setIsCheckoutSideMenuOpen] = useState(false);
const openCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(true);
const closeCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(false);
// Shopping Cart · Order
const [order, setOrder] = useState([]);
return (
<AppContext.Provider
value={
cart,
setCart,
openProductDetail,
closeProductDetail,
isProductDetailOpen,
productToShow,
setProductToShow,
cartProducts,
setCartProducts,
isCheckoutSideMenuOpen,
openCheckoutSideMenu,
closeCheckoutSideMenu,
order,
setOrder,
}
>
{children}
</AppContext.Provider>
);
};
value
is missing another set of{}
. It should look like this:value={ { ...content here.... } }
II. Modify the CheckoutSideMenu
Component
Located in src/Components/CheckoutSideMenu/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { useContext } from "react";
import { HiOutlineX } from "react-icons/hi";
import { AppContext } from "../../Context";
import OrderCard from "../OrderCard";
import { totalPrice } from "../../Utils/index.js";
const CheckoutSideMenu = () => {
const context = useContext(AppContext);
const handleDeleteProduct = (id) => {
const newCartProducts = context.cartProducts.filter(
(product) => product.id !== id
);
context.setCartProducts(newCartProducts);
context.setCart(context.cart - 1);
};
const handleCheckout = () => {
const orderToAdd = {
date: "2021-10-10",
products: context.cartProducts,
totalProducts: context.cartProducts.length,
totalPrice: totalPrice(context.cartProducts),
};
context.setOrder([...context.order, orderToAdd]);
context.setCartProducts([]);
};
return (
<aside
className={`${
context.isCheckoutSideMenuOpen ? "flex" : "hidden"
} flex-col fixed right-0 top-20 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2`}
>
<div className="flex justify-between items-center p-6">
<h2 className="font-medium">My Order</h2>
<div>
<HiOutlineX onClick={() => context.closeCheckoutSideMenu()} />
</div>
</div>
<div className="px-6 overflow-y-scroll flex-1">
{context.cartProducts.map((product) => (
<OrderCard
key={product.id}
id={product.id}
title={product.title}
// imageUrl={product.image} // Fake Store API
imageUrl={product.images} // Platzi API
price={product.price}
quantity={product.quantity}
handleDeleteProduct={handleDeleteProduct}
/>
))}
</div>
<div className="p-6">
<p className="flex justify-around items-center ">
<span className="font-light">Total</span>
<span className="font-medium">
${totalPrice(context.cartProducts)}
</span>
</p>
<button
className="w-full bg-black text-white font-medium py-2 rounded-lg mt-2 hover:bg-gray-900/50 transition duration-300"
onClick={() => handleCheckout()}
>
Checkout
</button>
</div>
</aside>
);
};
export default CheckoutSideMenu;
25. Checkout Products from Cart to My Last Order
I. Modify the CheckoutSideMenu
Component
Located in src/Components/CheckoutSideMenu/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { useContext } from "react";
import { Link } from "react-router-dom";
import { HiOutlineX } from "react-icons/hi";
import { AppContext } from "../../Context";
import OrderCard from "../OrderCard";
import { totalPrice } from "../../Utils/index.js";
const CheckoutSideMenu = () => {
const context = useContext(AppContext);
const handleDeleteProduct = (id) => {
const newCartProducts = context.cartProducts.filter(
(product) => product.id !== id
);
context.setCartProducts(newCartProducts);
context.setCart(context.cart - 1);
};
const handleCheckout = () => {
const orderToAdd = {
date: "2021-10-10",
products: context.cartProducts,
totalProducts: context.cartProducts.length,
totalPrice: totalPrice(context.cartProducts),
};
context.setOrder([...context.order, orderToAdd]);
context.setCartProducts([]);
};
return (
<aside
className={`${
context.isCheckoutSideMenuOpen ? "flex" : "hidden"
} flex-col fixed right-0 top-20 w-[360px] h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white/70 p-2 m-2`}
>
<div className="flex justify-between items-center p-6">
<h2 className="font-medium">My Order</h2>
<div>
<HiOutlineX onClick={() => context.closeCheckoutSideMenu()} />
</div>
</div>
<div className="px-6 overflow-y-scroll flex-1">
{context.cartProducts.map((product) => (
<OrderCard
key={product.id}
id={product.id}
title={product.title}
// imageUrl={product.image} // Fake Store API
imageUrl={product.images} // Platzi API
price={product.price}
quantity={product.quantity}
handleDeleteProduct={handleDeleteProduct}
/>
))}
</div>
<div className="p-6">
<p className="flex justify-around items-center ">
<span className="font-light">Total</span>
<span className="font-medium">
${totalPrice(context.cartProducts)}
</span>
</p>
<Link to="/my-orders/last">
<button
className="w-full bg-black text-white font-medium py-2 rounded-lg mt-2 hover:bg-gray-900/50 transition duration-300"
onClick={() => handleCheckout()}
>
Checkout
</button>
</Link>
</div>
</aside>
);
};
export default CheckoutSideMenu;
II. Modify the OrderCard
Component
Located in src/Components/OrderCard/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { HiOutlineTrash } from "react-icons/hi";
import { AppContext } from "../../Context";
import { useContext } from "react";
const OrderCard = (props) => {
const { id, title, imageUrl, price, handleDeleteProduct } = props;
let renderTrash;
if (handleDeleteProduct) {
renderTrash = (
<HiOutlineTrash
onClick={() => handleDeleteProduct(id)}
className="cursor-pointer"
/>
);
}
return (
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<figure className="w-20 h-20 m-0.5">
<img
className="w-full h-full rounded-lg object-cover"
src={imageUrl}
alt={title}
/>
</figure>
{/* <p className="text-sm font-light">{title}</p> */}
</div>
<div className="flex items-center gap-2">
<p className="text-lg font-medium">${price}</p>
<>{renderTrash} </>
</div>
</div>
);
};
export default OrderCard;
III. Modify the App
file
Located in src/Pages/App/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useRoutes, BrowserRouter } from "react-router-dom";
import { AppProvider } from "../../Context";
import Home from "../Home";
import MyAccount from "../MyAccount";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import SignIn from "../SignIn";
import Navbar from "../../Components/Navbar";
// import TestNavbar from "../../Components/TestJp/Navbar.jsx";
import "./App.css";
import CheckoutSideMenu from "../../Components/CheckoutSideMenu";
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/my-account", element: <MyAccount /> },
{ path: "/my-order", element: <MyOrder /> },
{ path: "/my-orders", element: <MyOrders /> },
{ path: "/my-orders/last", element: <MyOrder /> },
{ path: "/sign-in", element: <SignIn /> },
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<AppProvider>
<BrowserRouter>
<AppRoutes />
<Navbar />
<CheckoutSideMenu />
</BrowserRouter>
</AppProvider>
);
};
export default App;
IV. Modify the MyOrder
Component
Located in src/Components/MyOrder/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useContext } from "react";
import Layout from "../../Components/Layout";
import OrderCard from "../../Components/OrderCard";
import { AppContext } from "../../Context";
const MyOrder = () => {
const context = useContext(AppContext);
// console.log(context.order?.slice(-1)[0]);
return (
<Layout>
<h1>MyOrder</h1>
<div className="flex flex-col w-80">
{context.order?.slice(-1)[0].products.map((product) => (
<OrderCard
key={product.id}
id={product.id}
title={product.title}
// imageUrl={product.image} // Fake Store API
imageUrl={product.images} // Platzi API
price={product.price}
quantity={product.quantity}
/>
))}
</div>
</Layout>
);
};
export default MyOrder;
26. My Orders Page
I. Create the OrdersCard Component. (not OrderCard
)
OrdersCard is a NEW file. It is not the same as
OrderCard
.
Located in src/Components/OrdersCard/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// import {} from "react-icons/hi";
const OrdersCard = (props) => {
const { totalPrice, totalProducts } = props;
return (
<div className="flex justify-between items-center w-80 bg-slate-500">
<p className="flex-col justify-center items-center text-white text-2xl font-bold">
<span className="text-gray-400 text-xl p-5">date</span>
<span className="p-2">{totalProducts} items</span>
<span>${totalPrice}</span>
</p>
</div>
);
};
export default OrdersCard;
II. Modify the MyOrders
Page
Located in src/Pages/MyOrders/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useContext } from "react";
import Layout from "../../Components/Layout";
import { AppContext } from "../../Context";
import OrdersCard from "../../Components/OrdersCard";
import { Link } from "react-router-dom";
const MyOrders = () => {
const context = useContext(AppContext);
return (
<Layout>
<h1 className="">MyOrders</h1>
{context.order.map((order, index) => (
<Link to={`/my-orders/${index}`} key={index}>
<OrdersCard
totalProducts={order.totalProducts}
totalPrice={order.totalPrice}
/>
</Link>
))}
</Layout>
);
};
export default MyOrders;
III. Modify the MyOrder
Page
Located in src/Pages/MyOrder/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { useContext } from "react";
import Layout from "../../Components/Layout";
import OrderCard from "../../Components/OrderCard";
import { AppContext } from "../../Context";
import { HiChevronLeft } from "react-icons/hi";
import { Link } from "react-router-dom";
const MyOrder = () => {
const context = useContext(AppContext);
const currentPath = window.location.pathname;
let index = currentPath.substring(currentPath.lastIndexOf("/") + 1);
if (index === "last") {
index = context.order?.length - 1;
}
return (
<Layout>
<div className="flex items-center justify-center relative w-80">
<Link to="/my-orders" className="absolute left-0">
<HiChevronLeft className="h-4 w-4 cursor-pointer" />
</Link>
<h1 className="">My Order</h1>
</div>
<div className="flex flex-col w-80">
{context.order?.[index]?.products.map((product) => (
<OrderCard
key={product.id}
id={product.id}
title={product.title}
// imageUrl={product.image} // Fake Store API
imageUrl={product.images} // Platzi API
price={product.price}
quantity={product.quantity}
/>
))}
</div>
</Layout>
);
};
export default MyOrder;
IV. Modify the App
file
By adding a new route: { path: "/my-orders/:id", element: <MyOrder /> },
Located in src/Pages/App/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { useRoutes, BrowserRouter } from "react-router-dom";
import { AppProvider } from "../../Context";
import Home from "../Home";
import MyAccount from "../MyAccount";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import SignIn from "../SignIn";
import Navbar from "../../Components/Navbar";
// import TestNavbar from "../../Components/TestJp/Navbar.jsx";
import "./App.css";
import CheckoutSideMenu from "../../Components/CheckoutSideMenu";
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/my-account", element: <MyAccount /> },
{ path: "/my-order", element: <MyOrder /> },
{ path: "/my-orders", element: <MyOrders /> },
{ path: "/my-orders/last", element: <MyOrder /> },
{ path: "/my-orders/:id", element: <MyOrder /> },
{ path: "/sign-in", element: <SignIn /> },
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<AppProvider>
<BrowserRouter>
<AppRoutes />
<Navbar />
<CheckoutSideMenu />
</BrowserRouter>
</AppProvider>
);
};
export default App;
27. Refactor of my Home
Page
Refactor means to change the code of my project. In this case, I made a refactor to use items, setItems and other functionalities of my website that were only available in a local state. In this case it was only working on the home.jsx file and now I need to use them somewhere else.
I. Modify the Home
Component
Located in src/Components/Home/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useContext } from "react";
import Card from "../../Components/Card";
import Layout from "../../Components/Layout";
import ProductDetail from "../../Components/ProductDetail";
import { AppContext } from "../../Context";
const Home = () => {
const context = useContext(AppContext);
return (
<Layout>
<h1>Home</h1>
<div className="grid gap-4 grid-cols-4 w-full max-w-screen-lg">
{context.items?.map((item) => (
<Card key={item.id} data={item} />
))}
</div>
<ProductDetail />
</Layout>
);
};
export default Home;
II. Modify the Context
file
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { createContext, useEffect, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Home · Get Products - Fetch data from API
// UseState is a hook to add the info from the API to the state
const [items, setItems] = useState(null);
// UseEffect is a hook to fetch the data from the API
useEffect(() => {
// fetch("https://fakestoreapi.com/products") // Fake Store API
fetch("https://api.escuelajs.co/api/v1/products") // Platzi API
.then((response) => response.json())
.then((json) => setItems(json));
}, []);
// Shopping Cart · Increment quantity
const [count, setCount,] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
// Shopping Cart · add product to cart
const [cartProducts, setCartProducts] = useState([]);
// Checkout Side Menu · Open/Close
const [isCheckoutSideMenuOpen, setIsCheckoutSideMenuOpen] = useState(false);
const openCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(true);
const closeCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(false);
// Shopping Cart · Order
const [order, setOrder] = useState([]);
return (
<AppContext.Provider
value={
items,
setItems,
count,
setCount,
openProductDetail,
closeProductDetail,
isProductDetailOpen,
productToShow,
setProductToShow,
cartProducts,
setCartProducts,
isCheckoutSideMenuOpen,
openCheckoutSideMenu,
closeCheckoutSideMenu,
order,
setOrder,
}
>
{children}
</AppContext.Provider>
);
};
value
is missing another set of{}
. It should look like this:value={ { ...content here.... } }
28. Seach by Products
I. Modify the Home
Component
Add an input to search products
Located in src/Components/Home/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useContext } from "react";
import Card from "../../Components/Card";
import Layout from "../../Components/Layout";
import ProductDetail from "../../Components/ProductDetail";
import { AppContext } from "../../Context";
const Home = () => {
const context = useContext(AppContext);
return (
<Layout>
<div className="flex items-center justify-center relative w-80 mb-4">
<h1 className="font-medium text-xl">Products</h1>
</div>
<input
type="text"
placeholder="Search a product..."
className="border border-black rounded-xl w-96 px-4 py-2 mb-4"
onChange={(event) => context.setSearchByTitle(event.target.value)}
/>
<div className="grid gap-4 grid-cols-4 w-full max-w-screen-lg">
{context.items?.map((item) => (
<Card key={item.id} data={item} />
))}
</div>
<ProductDetail />
</Layout>
);
};
export default Home;
II. Modify the Context
file
Add the logic to search products. Searching by title(input)
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { createContext, useEffect, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Shopping Cart · Increment quantity
const [cart, setCart] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
// Shopping Cart · add product to cart
const [cartProducts, setCartProducts] = useState([]);
// Checkout Side Menu · Open/Close
const [isCheckoutSideMenuOpen, setIsCheckoutSideMenuOpen] = useState(false);
const openCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(true);
const closeCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(false);
// Shopping Cart · Order
const [order, setOrder] = useState([]);
// Get Products ·
// Fetch data from API · hook to add the info from the API to the state
const [items, setItems] = useState(null);
// Get Products · Search a product
const [searchByTitle, setSearchByTitle] = useState("");
console.log(searchByTitle);
// UseEffect is a hook to fetch the data from the API
useEffect(() => {
// fetch("https://fakestoreapi.com/products") // Fake Store API
fetch("https://api.escuelajs.co/api/v1/products") // Platzi API
.then((response) => response.json())
.then((json) => setItems(json));
}, []);
return (
<AppContext.Provider
value={
items,
setItems,
cart,
setCart,
openProductDetail,
closeProductDetail,
isProductDetailOpen,
productToShow,
setProductToShow,
cartProducts,
setCartProducts,
isCheckoutSideMenuOpen,
openCheckoutSideMenu,
closeCheckoutSideMenu,
order,
setOrder,
searchByTitle,
setSearchByTitle,
}
>
{children}
</AppContext.Provider>
);
};
value
is missing another set of{}
. It should look like this:value={ { ...content here.... } }
29. Filter by Title
I. Modify the Home
Component
Add a renderView
function to filter the products while typing.
Located in src/Components/Home/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { useContext } from "react";
import Card from "../../Components/Card";
import Layout from "../../Components/Layout";
import ProductDetail from "../../Components/ProductDetail";
import { AppContext } from "../../Context";
const Home = () => {
const context = useContext(AppContext);
const renderView = () => {
if (context.searchByTitle?.length > 0) {
if (context.filteredItems?.length > 0) {
return context.filteredItems?.map((item) => (
<Card key={item.id} data={item} />
));
} else {
return (
<div className="flex items-center justify-center w-full">
<h3 className="font-light text-md">No results found</h3>
</div>
);
}
} else {
return context.items?.map((item) => <Card key={item.id} data={item} />);
}
};
return (
<Layout>
<div className="flex items-center justify-center relative w-80 mb-4">
<h1 className="font-medium text-xl">Products</h1>
</div>
<input
type="text"
placeholder="Search a product..."
className="border border-black rounded-xl w-96 px-4 py-2 mb-4"
onChange={(event) => context.setSearchByTitle(event.target.value)}
/>
<div className="grid gap-4 grid-cols-4 w-full max-w-screen-lg">
{renderView()}
</div>
<ProductDetail />
</Layout>
);
};
export default Home;
II. Modify the Context
file
Add a Filter items by search
function (filteredItemsByTitle
)
As well as a useEffect
to implement the filtering.
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import { createContext, useEffect, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Shopping Cart · Increment quantity
const [cart, setCart] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
// Shopping Cart · add product to cart
const [cartProducts, setCartProducts] = useState([]);
// Checkout Side Menu · Open/Close
const [isCheckoutSideMenuOpen, setIsCheckoutSideMenuOpen] = useState(false);
const openCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(true);
const closeCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(false);
// Shopping Cart · Order
const [order, setOrder] = useState([]);
// console.log(order);
// Get Products ·
// Fetch data from API · hook to add the info from the API to the state
const [items, setItems] = useState(null);
// Get Products · Search a product
const [searchByTitle, setSearchByTitle] = useState("");
// console.log(searchByTitle);
// Filter items by search
const [filteredItems, setFilteredItems] = useState(null);
// UseEffect is a hook to fetch the data from the API
useEffect(() => {
// fetch("https://fakestoreapi.com/products") // Fake Store API
fetch("https://api.escuelajs.co/api/v1/products") // Platzi API
.then((response) => response.json())
.then((json) => setItems(json));
}, []);
// Filter items by search
const filteredItemsByTitle = (items, searchByTitle) => {
return items?.filter((item) =>
item.title.toLowerCase().includes(searchByTitle.toLowerCase())
);
};
// Filter items by search · useEffect
useEffect(() => {
if (searchByTitle) {
return setFilteredItems(filteredItemsByTitle(items, searchByTitle));
}
}, [items, searchByTitle]);
// console.log("filteredItems: ", filteredItems);
return (
<AppContext.Provider
value={
items,
setItems,
cart,
setCart,
openProductDetail,
closeProductDetail,
isProductDetailOpen,
productToShow,
setProductToShow,
cartProducts,
setCartProducts,
isCheckoutSideMenuOpen,
openCheckoutSideMenu,
closeCheckoutSideMenu,
order,
setOrder,
searchByTitle,
setSearchByTitle,
filteredItems,
setFilteredItems,
}
>
{children}
</AppContext.Provider>
);
};
value
is missing another set of{}
. It should look like this:value={ { ...content here.... } }
30. Filter by Category && Title
I. Modify the Navbar
Component
Add an onClick
event to filter by category (setSearchByCategory
) in the NavLink tag.
Located in src/Components/Navbar/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import { NavLink } from "react-router-dom";
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiOutlineShoppingCart } from "react-icons/hi";
const Navbar = () => {
const activeStyle = "underline text-gray-500 underline-offset-4";
const context = useContext(AppContext);
return (
<nav className="flex justify-between items-center fixed z-10 w-full py-5 px-8 text-md font-light top-0">
<ul className="flex items-center gap-3">
<li className="font-semibold text-lg">
<NavLink to="/" onClick={() => context.setSearchByCategory(null)}>
Shopi
</NavLink>
</li>
<li>
<NavLink
to="/"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory(null)}
>
All
</NavLink>
</li>
<li>
<NavLink
to="/clothes"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory("clothes")}
>
Clothes
</NavLink>
</li>
<li>
<NavLink
to="/electronics"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory("electronics")}
>
Electronics
</NavLink>
</li>
<li>
<NavLink
to="/furnitures"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory("furnitures")}
>
Furnitures
</NavLink>
</li>
<li>
<NavLink
to="/toys"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory("toys")}
>
Toys
</NavLink>
</li>
</ul>
<ul className="flex items-center gap-3">
<li>
<NavLink
to="/my-orders"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
My Orders
</NavLink>
</li>
<li>
<NavLink
to="/my-account"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
My Account
</NavLink>
</li>
<li>
<NavLink
to="/sign-in"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
Sign In
</NavLink>
</li>
<li>
<NavLink
to="/card"
className={`flex justify-center items-center ${({ isActive }) =>
isActive ? activeStyle : undefined}`}
>
<HiOutlineShoppingCart className="mr-1" />
<p>{context.cartProducts.length}</p>
</NavLink>
</li>
</ul>
</nav>
);
};
export default Navbar;
II. Modify the App
file
By adding all the new routes: { path: "/newPath", element: <Home /> }
Located in src/Pages/App/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { useRoutes, BrowserRouter } from "react-router-dom";
import { AppProvider } from "../../Context";
import Home from "../Home";
import MyAccount from "../MyAccount";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import SignIn from "../SignIn";
import Navbar from "../../Components/Navbar";
// import TestNavbar from "../../Components/TestJp/Navbar.jsx";
import "./App.css";
import CheckoutSideMenu from "../../Components/CheckoutSideMenu";
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/clothes", element: <Home /> },
{ path: "/electronics", element: <Home /> },
{ path: "/furnitures", element: <Home /> },
{ path: "/toys", element: <Home /> },
{ path: "/others", element: <Home /> },
{ path: "/my-account", element: <MyAccount /> },
{ path: "/my-order", element: <MyOrder /> },
{ path: "/my-orders", element: <MyOrders /> },
{ path: "/my-orders/last", element: <MyOrder /> },
{ path: "/my-orders/:id", element: <MyOrder /> },
{ path: "/sign-in", element: <SignIn /> },
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<AppProvider>
<BrowserRouter>
<AppRoutes />
<Navbar />
<CheckoutSideMenu />
</BrowserRouter>
</AppProvider>
);
};
export default App;
III. Modify the Context
file
By adding all the logic to filter the render products by title
, category
, title and category
.
In other words, render the view depending on the action:
- Display all the products with NO filters.
- Display product/s when searching (filtering
title
) - Display product/s by filtering
category
- Display by filtering
title and category
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import { createContext, useEffect, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Shopping Cart · Increment quantity
const [cart, setCart] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
// Shopping Cart · add product to cart
const [cartProducts, setCartProducts] = useState([]);
// Checkout Side Menu · Open/Close
const [isCheckoutSideMenuOpen, setIsCheckoutSideMenuOpen] = useState(false);
const openCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(true);
const closeCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(false);
// Shopping Cart · Order
const [order, setOrder] = useState([]);
// console.log(order);
// Get Products
// Fetch data from API · hook to add the info from the API to the state
const [items, setItems] = useState(null);
// UseEffect is a hook to fetch the data from the API
useEffect(() => {
// fetch("https://fakestoreapi.com/products") // Fake Store API
fetch("https://api.escuelajs.co/api/v1/products") // Platzi API
.then((response) => response.json())
.then((json) => setItems(json));
}, []);
// Get Products · Search a Product
const [searchByTitle, setSearchByTitle] = useState(null);
// console.log(searchByTitle);
// Get Products · Filter items by category
const [searchByCategory, setSearchByCategory] = useState(null);
// console.log("searchByCategory: ", searchByCategory);
// Filter items by search
const [filteredItems, setFilteredItems] = useState(null);
const filteredItemsByTitle = (items, searchByTitle) => {
return items?.filter((item) =>
item.title.toLowerCase().includes(searchByTitle.toLowerCase())
);
};
const filteredItemsByCategory = (items, searchByCategory) => {
return items?.filter((item) =>
item.category.name.toLowerCase().includes(searchByCategory.toLowerCase())
);
};
const filterBy = (searchType, items, searchByTitle, searchByCategory) => {
// Filter by title
if (searchType === "BY_TITLE") {
return filteredItemsByTitle(items, searchByTitle);
}
// Filter by category
if (searchType === "BY_CATEGORY") {
return filteredItemsByCategory(items, searchByCategory);
}
// Filter by title and category
if (searchType === "BY_TITLE_AND_CATEGORY") {
return filteredItemsByCategory(items, searchByCategory).filter((item) =>
item.title.toLowerCase().includes(searchByTitle.toLowerCase())
);
}
// The is NO Filter, return all items
if (!searchType) {
return items;
}
};
useEffect(() => {
// Filter by title and category
if (searchByTitle && searchByCategory) {
return setFilteredItems(
filterBy(
"BY_TITLE_AND_CATEGORY",
items,
searchByTitle,
searchByCategory
)
);
}
// Filter by title
if (searchByTitle && !searchByCategory) {
return setFilteredItems(
filterBy("BY_TITLE", items, searchByTitle, searchByCategory)
);
}
// Filter by category
if (!searchByTitle && searchByCategory) {
return setFilteredItems(
filterBy("BY_CATEGORY", items, searchByTitle, searchByCategory)
);
}
// No Filter, return all items
if (!searchByTitle && !searchByCategory) {
return setFilteredItems(
filterBy(null, items, searchByTitle, searchByCategory)
);
}
}, [items, searchByTitle, searchByCategory]);
console.log("searchByCategory: ", searchByCategory);
console.log("searchByTitle: ", searchByTitle);
console.log("filteredItems: ", filteredItems);
return (
<AppContext.Provider
value={
items,
setItems,
cart,
setCart,
openProductDetail,
closeProductDetail,
isProductDetailOpen,
productToShow,
setProductToShow,
cartProducts,
setCartProducts,
isCheckoutSideMenuOpen,
openCheckoutSideMenu,
closeCheckoutSideMenu,
order,
setOrder,
filteredItems,
setFilteredItems,
searchByTitle,
setSearchByTitle,
searchByCategory,
setSearchByCategory,
}
>
{children}
</AppContext.Provider>
);
};
value
is missing another set of{}
. It should look like this:value={ { ...content here.... } }
IV. Modify the Home
Component
Located in src/Components/Home/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { useContext } from "react";
import Card from "../../Components/Card";
import Layout from "../../Components/Layout";
import ProductDetail from "../../Components/ProductDetail";
import { AppContext } from "../../Context";
const Home = () => {
const context = useContext(AppContext);
const renderView = () => {
// if there are items in the filteredItems array, render them
// Filter by title and category
if (context.filteredItems?.length > 0) {
return context.filteredItems?.map((item) => (
<Card key={item.id} data={item} />
));
} else {
return (
<div className="flex flex-col gap-3 w-full">
<h3 className="font-light text-md">
No results for:
<span className="font-medium text-md text-gray-400 pl-2">
{context.searchByTitle}
</span>
</h3>
<p className="font-light text-sm text-gray-400/80">
Try searching for another product
</p>
</div>
);
}
};
return (
<Layout>
<div className="flex items-center justify-center relative w-80 mb-4">
<h1 className="font-medium text-xl">Products</h1>
</div>
<input
type="text"
placeholder="Search a product..."
className="border border-black rounded-xl w-96 px-4 py-2 mb-4"
onChange={(event) => context.setSearchByTitle(event.target.value)}
/>
<div className="grid gap-4 grid-cols-4 w-full max-w-screen-lg">
{renderView()}
</div>
<ProductDetail />
</Layout>
);
};
export default Home;
31. Testing and debugging
This is one of the most important parts.
I. Cart Count
One problem I found was that the cart count was an independent number and need it to be the same as the order. I change it to the lenght of the array.
- Modify the
Navbar
Component
Located in src/Components/Navbar/index.jsx
1
2
3
4
5
// Stop using the cart value:
context.cart;
// And use the lenght of the products array
context.cartProducts.length;
II. Checkout Button
After placing an order, if the user previously typed something on the screen and didn’t refresh the page, that input remained saved on the setSearchByTitle
. I had to reset it after checkout.
- Check the values in
Context
file
1
2
3
console.log("searchByCategory: ", searchByCategory);
console.log("searchByTitle: ", searchByTitle);
console.log("filteredItems: ", filteredItems);
- Modify the
CheckoutSideMenu
Component
Located in src/Components/CheckoutSideMenu/index.jsx
1
2
3
4
5
6
7
8
// under handleCheckout, reset Title and Category
const handleCheckout = () => {
// ... code ...
context.setSearchByTitle(null); // Reset the search
context.setSearchByCategory(null); // Reset the search
};
32. Improve the UI (User Interface) and UX (User Experience)
I. Make the Site Responsive
Responsive design is a part of both UI (User Interface) and UX (User Experience). From a UI perspective, it involves designing interfaces that adapt to different screen sizes. From a UX perspective, it ensures that the user has a seamless experience on any device.
II. Change the Icons
Changing the icons is a part of both UI (User Interface) and UX (User Experience). From a UI perspective, it involves updating the visual elements of the interface. From a UX perspective, it can improve the user’s interaction with the system by making icons more intuitive or easier to understand.
III. Modify the Styling of some pages
Styling is a part of both UI (User Interface) and UX (User Experience). From a UI perspective, it involves choosing colors, fonts, and layouts. From a UX perspective, good styling can make the interface easier to use and more enjoyable for the user.
33. Clean Up the Project
It’s good practice to check if we are repeating code and clean up the project. Here is an example of code that we can clean.
In other words, refactoring the code to avoid repetition its good practice.
Modify the Navbar
Component
Refactor the code to avoid repetition by creating a new component for the navigation links.
Located in src/Components/Navbar/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { NavLink } from "react-router-dom";
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiOutlineShoppingCart } from "react-icons/hi";
import { useScrollPosition } from "../../Utils/useScrollPosition";
// refactored code to avoid repetition. activeStyle was getting repeated.
const NavItem = ({ to, children, setSearchByCategory }) => {
const activeStyle = "underline text-gray-500 underline-offset-4";
return (
<li>
<NavLink
to={to}
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => setSearchByCategory && setSearchByCategory(to.slice(1))}
>
{children}
</NavLink>
</li>
);
};
const Navbar = () => {
const context = useContext(AppContext);
function classNamesNavBarScroll(...classes) {
return classes.filter(Boolean).join(" ");
}
const scrollPosition = useScrollPosition();
// console.log(scrollPosition);
return (
<header
className={classNamesNavBarScroll(
scrollPosition > 0
? "md:shadow md:bg-white md:-translate-y-6 md:h-auto"
: "md:shadow-none md:bg-none md:translate-y-0 md:h-none",
"absolute md:fixed top-2 inset-x-0 z-40 md:transition-shadow-xl md:shadow-black md:transition-color duration-500 md:-translate-y-6 md:h-20 lg:h-14"
)}
>
<nav className="hidden sm:flex flex-col sm:flex-row justify-between items-center fixed z-10 w-full py-5 px-8 text-md font-light top-0">
<ul className="flex flex-col sm:flex-row items-center gap-3">
<li className="font-semibold text-lg">
<NavLink to="/" onClick={() => context.setSearchByCategory(null)}>
Shopi
</NavLink>
</li>
<NavItem to="/" setSearchByCategory={context.setSearchByCategory}>
All
</NavItem>
<NavItem
to="/clothes"
setSearchByCategory={context.setSearchByCategory}
>
Clothes
</NavItem>
<NavItem
to="/electronics"
setSearchByCategory={context.setSearchByCategory}
>
Electronics
</NavItem>
<NavItem
to="/furnitures"
setSearchByCategory={context.setSearchByCategory}
>
Furnitures
</NavItem>
<NavItem to="/toys" setSearchByCategory={context.setSearchByCategory}>
Toys
</NavItem>
</ul>
<ul className="hidden sm:flex items-center gap-3">
<NavItem to="/my-orders">My Orders</NavItem>
<NavItem to="/sign-in">Sign In</NavItem>
<NavItem to="/my-account">My Account</NavItem>
<li>
<NavLink
to="/card"
className={`flex justify-center items-center ${({ isActive }) =>
isActive ? activeStyle : undefined}`}
>
<HiOutlineShoppingCart className="mr-1" />
<p>{context.cartProducts.length}</p>
</NavLink>
</li>
</ul>
</nav>
</header>
);
};
export default Navbar;
In this refactored code, a new NavItem component is created which encapsulates the repeated NavLink code. The NavItem component takes a to prop for the link, children for the link text, and an optional setSearchByCategory function. If setSearchByCategory is provided, it will be called with the category name when the link is clicked.
Notice that the “Shopi” item has its own unique styles and does not share the same styles as the other NavItem components, you can directly use the NavLink component for it instead of the NavItem component.
34. Create Private Routes and Public Routes
Note: I implemented the private & public router in another project called JP·Shop. Shopi does not have a working sign in, sign out, my account features.
Private and Public Routes refer to the accessibility of certain routes (or pages) based on the user’s authentication status.
Private Routes: These are routes that are only accessible to authenticated users. If a user is not authenticated and tries to access a private route, they are typically redirected to a login page.
Public Routes: These are routes that are accessible to all users, regardless of their authentication status. Examples of public routes might include the login page, the registration page, or a public homepage.
How can I implement Private and Public Routes in React?
- useAuth: login y logout
I. Create an auth.jsx
file in context
Locate in src/Context/auth.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { useContext } from "react";
import { useNavigate } from "react-router-dom";
const AuthContext = React.createContext();
function AuthProvider({ children }) {
const navigate = useNavigate();
const [user, setUser] = React.useState(null);
const login = (username, password) => {
if (typeof username !== "string") {
throw new Error("Username must be a string");
}
setUser({ username, password });
navigate("/my-account");
};
const logout = () => {
setUser(null);
navigate("/");
};
const auth = { user, login, logout };
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}
function useAuth() {
//here?
const auth = React.useContext(AuthContext);
return auth;
}
export { AuthProvider, useAuth };
II. Modify the SignIn.jsx
page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import React, { useState } from "react";
import Layout from "../../Components/Layout";
import { useAuth } from "../../Context/auth";
function SignIn() {
const auth = useAuth(); // console.log("auth: ", auth);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleLogin = (e) => {
e.preventDefault();
// console.log("username: ", username);
// console.log("password: ", password);
auth.login(username, password);
// onLogin(username, password); this is from copilot-signin. and onlogin needs to be passed in as a prop. at the beginning of... function SignIn({ onLogin }) {
};
return (
<Layout>
<div className="flex items-center justify-center relative mb-4">
<h1 className="font-medium text-md sm:text-xl">Sign In</h1>
</div>
<form
onSubmit={handleLogin}
className="flex flex-col justify-center items-center w-screen h-screen gap-5"
>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
className="border border-gray-300 rounded-md px-2 py-1"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="border border-gray-300 rounded-md px-2 py-1"
/>
<button
type="submit"
className="w-48 bg-black text-white font-medium py-2 rounded-lg mt-2 hover:bg-gray-900/50 transition duration-300"
>
Sign In
</button>
</form>
</Layout>
);
}
export default SignIn;
III. Modify the MyAccount.jsx
page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Layout from "../../Components/Layout";
import { useAuth } from "../../Context/auth";
const MyAccount = () => {
const auth = useAuth();
return (
<Layout>
<div className="flex flex-col gap-7 items-center justify-center relative mb-4">
<h1 className="font-medium text-md sm:text-xl">My Account</h1>
<p className="font-semibold text-2xl">Welcome, {auth.user.username}</p>
{console.log("username: ", auth.user.username)}
{console.log("password: ", auth.user.password)}
</div>
</Layout>
);
};
export default MyAccount;
IV. Modify the App.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { useRoutes, BrowserRouter } from "react-router-dom";
import { AppProvider } from "../../Context";
import { AuthProvider } from "../../Context/auth"; // AuthContext is the context that will be used to store the user's data
import Navbar from "../../Components/Navbar";
import CheckoutSideMenu from "../../Components/CheckoutSideMenu";
import "./App.css";
import Home from "../Home";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import MyAccount from "../MyAccount";
import SignIn from "../SignIn";
import Logout from "../Logout";
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/smartphones", element: <Home /> },
{ path: "/laptops", element: <Home /> },
{ path: "/fragrances", element: <Home /> },
{ path: "/skincare", element: <Home /> },
{ path: "/groceries", element: <Home /> },
{ path: "/home-decoration", element: <Home /> },
// Should be Private Route but for testing purposes it is public
{ path: "/my-order", element: <MyOrder /> },
{ path: "/my-orders", element: <MyOrders /> },
{ path: "/my-orders/last", element: <MyOrder /> },
{ path: "/my-orders/:id", element: <MyOrder /> },
// Private Routes
{ path: "/my-account", element: <MyAccount /> },
{ path: "/sign-in", element: <SignIn /> },
{ path: "/logout", element: <Logout /> },
// Not Found
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<AppProvider>
<BrowserRouter>
<AuthProvider>
<AppRoutes />
<Navbar />
<CheckoutSideMenu />
</AuthProvider>
</BrowserRouter>
</AppProvider>
);
};
export default App;
V. Modify the Navbar.jsx
Component
In your Navbar component, you can use the auth context to conditionally render links based on whether the user is authenticated or not. For example, you might only want to show the “My Account” and “Logout” links if the user is authenticated, and only show the “Sign In” link if the user is not authenticated.
Here’s how you can do that:
- Option 1: ```jsx
-
{auth.user && (
- <NavLink to="/my-account" className={({ isActive }) => (isActive ? activeStyle : undefined)} > My Account </NavLink> )} {auth.user ? (
- <NavLink to="/Logout" className={({ isActive }) => (isActive ? activeStyle : undefined)} > Logout </NavLink> ) : (
- <NavLink to="/sign-in" className={({ isActive }) => (isActive ? activeStyle : undefined)} > Sign In </NavLink> )}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
> if auth.user is truthy (i.e., the user is authenticated), the "Logout" and "My Account" links are rendered. If auth.user is falsy (i.e., the user is not authenticated), the "Sign In" link is rendered.
- Option 2:
```jsx
// ...
<ul className="hidden sm:flex items-center gap-3">
{auth.user && (
<>
<li>
<NavLink
to="/my-account"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
My Account
</NavLink>
</li>
<li>
<NavLink
to="/my-orders"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
>
My Orders
</NavLink>
</li>
<li>
<NavLink
to="/card"
className={`flex justify-center items-center ${({ isActive }) =>
isActive ? activeStyle : undefined}`}
>
<HiOutlineShoppingCart className="mr-1" />
<p>{context.cartProducts.length}</p>
</NavLink>
</li>
<li>
<NavLink
to="/Logout"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
Logout
</NavLink>
</li>
</>
)}
{!auth.user && (
<li>
<NavLink
to="/sign-in"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
Sign In
</NavLink>
</li>
)}
</ul>
// ...
You can also have the links to “My Account”, “My Orders”, “Card”, and “Logout” will only be shown if auth.user is truthy, indicating that a user is authenticated. If auth.user is falsy, indicating that no user is authenticated, only the “Sign In” link will be shown.
Here’s how the Navbar.jsx
should look like using option 2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import { NavLink } from "react-router-dom";
import { useContext, useState } from "react";
import { AppContext } from "../../Context";
import { HiOutlineShoppingCart } from "react-icons/hi";
import { useScrollPosition } from "../../Utils/useScrollPosition";
import { useAuth } from "../../Context/auth";
const Navbar = () => {
const activeStyle = "underline text-gray-500 underline-offset-4";
const context = useContext(AppContext);
//scrollPosition:
const [showDropdown, setShowDropdown] = useState(false);
const [showDropdownTech, setShowDropdownTech] = useState(false);
function classNamesNavBarScroll(...classes) {
return classes.filter(Boolean).join(" ");
}
const scrollPosition = useScrollPosition();
// console.log(scrollPosition);
// AuthContext:
const auth = useAuth();
console.log("in Navbar, Auth.user: ", auth.user);
return (
<header
className={classNamesNavBarScroll(
scrollPosition > 0
? "md:shadow md:bg-white md:-translate-y-6 md:h-auto"
: "md:shadow-none md:bg-none md:translate-y-0 md:h-none",
"absolute md:fixed top-2 inset-x-0 z-40 md:transition-shadow-xl md:shadow-black md:transition-color duration-500 md:-translate-y-6 md:h-20 lg:h-14"
)}
>
<nav className="hidden sm:flex flex-col sm:flex-row justify-between items-center fixed z-10 w-full py-5 px-8 text-md font-light top-0">
<ul className="flex flex-col sm:flex-row items-center gap-3">
<li className="font-semibold text-lg">
<NavLink to="/" onClick={() => context.setSearchByCategory(null)}>
JP·Shop
</NavLink>
</li>
<li>
<NavLink
to="/"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory(null)}
>
All
</NavLink>
</li>
<li onClick={() => setShowDropdownTech(!showDropdownTech)}>
Electronics
{showDropdownTech && (
<div>
<NavLink
to="/smartphones"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
onClick={() => context.setSearchByCategory("smartphones")}
>
Phone
</NavLink>
<NavLink
to="/laptops"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
onClick={() => context.setSearchByCategory("laptops")}
>
Laptop
</NavLink>
</div>
)}
</li>
<li onClick={() => setShowDropdown(!showDropdown)}>
Cosmetics
{showDropdown && (
<div>
<NavLink
to="/fragrances"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
onClick={() => context.setSearchByCategory("fragrances")}
>
Perfumes
</NavLink>
<NavLink
to="/skincare"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
onClick={() => context.setSearchByCategory("skincare")}
>
Skin Care
</NavLink>
</div>
)}
</li>
<li>
<NavLink
to="/groceries"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory("groceries")}
>
Groceries
</NavLink>
</li>
<li>
<NavLink
to="/home-decoration"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory("home-decoration")}
>
HomeGoods
</NavLink>
</li>
</ul>
<ul className="hidden sm:flex items-center gap-3">
{auth.user && (
<>
<li>
<NavLink
to="/my-account"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
My Account
</NavLink>
</li>
<li>
<NavLink
to="/my-orders"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
My Orders
</NavLink>
</li>
<li>
<NavLink
to="/card"
className={`flex justify-center items-center ${({
isActive,
}) => (isActive ? activeStyle : undefined)}`}
>
<HiOutlineShoppingCart className="mr-1" />
<p>{context.cartProducts.length}</p>
</NavLink>
</li>
<li>
<NavLink
to="/Logout"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
Logout
</NavLink>
</li>
</>
)}
{!auth.user && (
<li>
<NavLink
to="/sign-in"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
Sign In
</NavLink>
</li>
)}
</ul>
</nav>
</header>
);
};
export default Navbar;
Private and Public Routes
To create private and public routes, you can create two components: PrivateRoute and PublicRoute.
PrivateRoute will check if the user is authenticated. If they are, it will render the component passed to it. If not, it will redirect the user to the login page.
PublicRoute will check if the user is authenticated. If they are, it will redirect them to the “My Account” (or another page). If not, it will render the component passed to it.
The PrivateRoute and PublicRoute are components that you will create to handle private and public routes in your application. They will use the useAuth hook to check if a user is authenticated and the useNavigate hook to redirect users based on their authentication status.
Here’s how you can create these components:
I. Modify the App.jsx
Page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { useRoutes, BrowserRouter, Navigate } from "react-router-dom";
import { AppProvider } from "../../Context";
import { AuthProvider } from "../../Context/auth"; // AuthContext is the context that will be used to store the user's data
import Navbar from "../../Components/Navbar";
import CheckoutSideMenu from "../../Components/CheckoutSideMenu";
import "./App.css";
import Home from "../Home";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import MyAccount from "../MyAccount";
import SignIn from "../SignIn";
import Logout from "../Logout";
// Implementing the Private and Public Routes:
import { useAuth } from "../../Context/auth"; // make sure you have a useAuth hook in your auth context
const PrivateRoute = ({ children }) => {
const { user } = useAuth();
return user ? children : <Navigate to="/sign-in" />;
};
const PublicRoute = ({ children }) => {
const { user } = useAuth();
return user ? <Navigate to="/" /> : children;
};
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/smartphones", element: <Home /> },
{ path: "/laptops", element: <Home /> },
{ path: "/fragrances", element: <Home /> },
{ path: "/skincare", element: <Home /> },
{ path: "/groceries", element: <Home /> },
{ path: "/home-decoration", element: <Home /> },
// Private Routes
{ path: "/my-order", element: <PrivateRoute><MyOrder /></PrivateRoute> },
{ path: "/my-orders", element: <PrivateRoute><MyOrders /></PrivateRoute> },
{ path: "/my-orders/last", element: <PrivateRoute><MyOrder /></PrivateRoute> },
{ path: "/my-orders/:id", element: <PrivateRoute><MyOrder /></PrivateRoute> },
{ path: "/my-account", element: <PrivateRoute><MyAccount /></PrivateRoute> },
{ path: "/logout", element: <PrivateRoute><Logout /></PrivateRoute> },
// Public Routes
{ path: "/sign-in", element: <PublicRoute><SignIn /></PublicRoute> },
// Not Found
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<AppProvider>
<BrowserRouter>
<AuthProvider>
<AppRoutes />
<Navbar />
<CheckoutSideMenu />
</AuthProvider>
</BrowserRouter>
</AppProvider>
);
};
export default App;
The code use the useAuth hook from auth context that returns the current user. If the user is null or undefined, it means that the user is not authenticated.
35. Add Product to Cart From ProductDetail
page (or any page)
To use the addProductToCart
function in your ProductDetail
page, you need to move this function to the context so that it can be shared between different components. Here’s how you can do it:
I. Modify the Context
file
In your AppContext, add a new function addProductToCart.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { createContext, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
const [count, setCount] = useState(0);
const [cartProducts, setCartProducts] = useState([]);
// other states...
const addProductToCart = (productData) => {
setCount(count + 1);
setCartProducts([...cartProducts, productData]);
// other actions...
};
return (
<AppContext.Provider
value=9
>
{children}
</AppContext.Provider>
);
};
Context
should look something like this:
Located in src/Context/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { createContext, useEffect, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Get Products · State to store the data from the dummy API. It's an empty array because the data is an array of objects
// Fetch data from API · hook to add the info from the API to the state
const [items, setItems] = useState([]);
// UseEffect is a hook to fetch the data from the API
useEffect(() => {
fetch("https://dummyjson.com/products")
.then((response) => response.json())
.then((json) => {
// console.log("Data from Dummy API: ", json); // Log the data
// console.log("Products inside Data Dummy API: ", json.products); // Products is an array of objects inside the data from the API
setItems(json.products); // Add the data to the state (setItems) and specify the data to be added (json.products)
});
}, []);
// Shopping Cart · Increment quantity
const [count, setCount] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
// Shopping Cart · add product to cart
const [cartProducts, setCartProducts] = useState([]);
// Checkout Side Menu · Open/Close
const [isCheckoutSideMenuOpen, setIsCheckoutSideMenuOpen] = useState(false);
const openCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(true);
const closeCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(false);
// Shopping Cart · Order
const [order, setOrder] = useState([]);
// add product to cart
const addProductToCart = (productData) => {
setCount(count + 1);
setCartProducts([...cartProducts, productData]);
// console.log("cartProducts: ", cartProducts);
// console.log("productData: ", productData);
openCheckoutSideMenu();
closeProductDetail();
};
// Get Products · Search a product
// const [searchByTitle, setSearchByTitle] = useState("");
const [searchByTitle, setSearchByTitle] = useState(null);
// console.log(searchByTitle);
// Get Products · Filter items by category
const [searchByCategory, setSearchByCategory] = useState(null);
// console.log("searchByCategory: ", searchByCategory);
// Filter items by search
const [filteredItems, setFilteredItems] = useState(null);
// Filter items by search
const filteredItemsByTitle = (items, searchByTitle) => {
return items?.filter((item) =>
item.title.toLowerCase().includes(searchByTitle.toLowerCase())
);
};
// Filter items by category
const filteredItemsByCategory = (items, searchByCategory) => {
return items?.filter((item) =>
item.category.toLowerCase().includes(searchByCategory.toLowerCase())
);
};
const filterBy = (searchType, items, searchByTitle, searchByCategory) => {
// Filter by title
if (searchType === "BY_TITLE") {
return filteredItemsByTitle(items, searchByTitle);
}
// Filter by category
if (searchType === "BY_CATEGORY") {
return filteredItemsByCategory(items, searchByCategory);
}
// Filter by title and category
if (searchType === "BY_TITLE_AND_CATEGORY") {
return filteredItemsByCategory(items, searchByCategory).filter((item) =>
item.title.toLowerCase().includes(searchByTitle.toLowerCase())
);
}
// The is NO Filter, return all items
if (!searchType) {
return items;
}
};
useEffect(() => {
// Filter by title and category
if (searchByTitle && searchByCategory) {
return setFilteredItems(
filterBy(
"BY_TITLE_AND_CATEGORY",
items,
searchByTitle,
searchByCategory
)
);
}
// Filter by title
if (searchByTitle && !searchByCategory) {
return setFilteredItems(
filterBy("BY_TITLE", items, searchByTitle, searchByCategory)
);
}
// Filter by category
if (!searchByTitle && searchByCategory) {
return setFilteredItems(
filterBy("BY_CATEGORY", items, searchByTitle, searchByCategory)
);
}
// No Filter, return all items
if (!searchByTitle && !searchByCategory) {
return setFilteredItems(
filterBy(null, items, searchByTitle, searchByCategory)
);
}
}, [items, searchByTitle, searchByCategory]);
// console.log("searchByCategory: ", searchByCategory);
// console.log("searchByTitle: ", searchByTitle);
// console.log("filteredItems: ", filteredItems);
return (
<AppContext.Provider
value=
>
{children}
</AppContext.Provider>
);
};
II. Modify the Card
Component
In your Card component, use the addProductToCart from context.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useContext } from "react";
import { AppContext } from "../../Context";
const Card = (data) => {
const { addProductToCart } = useContext(AppContext);
// other code...
const renderIcon = (id) => {
// other code...
return (
<TbShoppingCartPlus
onClick={(event) => {
event.stopPropagation();
addProductToCart(data.data);
}}
className="w-4 h-4"
/>
);
};
// other code...
};
Card
should look something like this:
Located in src/Components/Card/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { useContext } from "react";
import { AppContext } from "../../Context";
import { HiCheck } from "react-icons/hi";
import { TbShoppingCartPlus } from "react-icons/tb";
const Card = (data) => {
const context = useContext(AppContext);
const { addProductToCart } = useContext(AppContext);
const showProduct = (productDetail) => {
context.openProductDetail();
context.setProductToShow(productDetail);
context.closeCheckoutSideMenu();
};
// Check if the product is in the cart:
const renderIcon = (id) => {
if (data && data.data && data.data.id) {
const productIsInCart =
context.cartProducts.filter((product) => product.id === id).length > 0;
if (productIsInCart) {
return (
<div className="absolute top-0 right-0 flex justify-center items-center bg-black text-white rounded-full border-none m-2 p-1">
<HiCheck className="w-4 h-4" />
</div>
);
} else {
return (
<div className="absolute top-0 right-0 flex justify-center items-center bg-white/50 hover:bg-white transition duration-300 rounded-full border-none m-2 p-1">
<TbShoppingCartPlus
onClick={(event) => {
event.stopPropagation();
addProductToCart(data.data);
}}
className="w-4 h-4"
/>
</div>
);
}
}
return null;
};
return (
<div
className="bg-white border shadow-lg cursor-pointer w-56 h-60 rounded-lg hover:scale-105 transition duration-300"
onClick={() => showProduct(data.data)}
>
<figure className="relative mb-2 w-full h-4/5">
<span className="absolute bottom-0 bg-white/60 rounded-lg text-black text-xs m-2 py-0.5 px-2">
{data.data.category}
</span>
<img
className="rounded-lg w-full h-full object-scale-down"
src={data.data.images[0]}
alt={data.data.title}
/>
{renderIcon(data.data.id)}
</figure>
<p className="flex justify-around">
<span className="text-sm font-light">
{data.data.title.split(" ").slice(0, 3).join(" ")}
</span>
<span className="text-lg font-medium">${data.data.price}</span>
</p>
</div>
);
};
export default Card;
III. Modify the ProductDetail
Component
In the ProductDetail component, use the addProductToCart from context.
1
2
3
4
5
6
7
8
9
10
11
12
import { useContext } from "react";
import { AppContext } from "../../Context";
const ProductDetail = (data) => {
const { addProductToCart } = useContext(AppContext);
// other code...
return (
<button onClick={() => addProductToCart(data.data)}>Add to cart</button>
);
};
ProductDetail
should look something like this:
Located in src/Components/ProductDetail/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import {
HiOutlineX,
HiOutlineTag,
HiOutlineShoppingCart,
HiOutlineStar,
HiOutlineCash,
HiOutlineTruck,
HiOutlinePhotograph,
HiOutlineDocumentText,
HiOutlineBadgeCheck,
HiOutlineArrowNarrowLeft,
HiOutlineArrowNarrowRight,
} from "react-icons/hi";
import { useContext, useState } from "react";
import { AppContext } from "../../Context";
import { BeatLoader } from "react-spinners"; // npm install react-spinners
const ProductDetail = (data) => {
const context = useContext(AppContext);
// console.log("context.productToShow: ", context.productToShow);
const { addProductToCart } = useContext(AppContext);
// Inside your component
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [isLoading, setIsLoading] = useState(false); // for image loading
const handleNext = () => {
setCurrentImageIndex((prevIndex) =>
prevIndex === context.productToShow?.images?.length - 1
? 0
: prevIndex + 1
);
};
const handlePrev = () => {
setCurrentImageIndex((prevIndex) =>
prevIndex === 0
? context.productToShow?.images?.length - 1
: prevIndex - 1
);
};
// tailwind css classes for table:
const tdElements = "flex justify-start items-center gap-2 py-0.5";
return (
<aside
className={`${
context.isProductDetailOpen ? "flex" : "hidden"
} flex-col fixed right-0 top-20 w-[360px] h-min sm:h-[90vh] border border-black shadow-xl shadow-black rounded-lg bg-white sm:bg-white/70 p-2 m-2`}
>
<div className="flex justify-between items-center p-6">
<h2 className="font-medium">Product Detail</h2>
<HiOutlineX onClick={() => context.closeProductDetail()} />
</div>
{/* <figure className="flex justify-center items-center px-6">
<img
className="w-fit h-60 rounded-lg"
src={context.productToShow?.images?.[0]}
alt={context.productToShow?.title}
/>
</figure> */}
{/* Image Slices: */}
<figure className="flex justify-center items-center px-6">
{isLoading ? (
<BeatLoader color="#123abc" />
) : (
context.productToShow?.images?.[currentImageIndex] && (
<img
className="w-fit h-60 rounded-lg"
src={context.productToShow?.images[currentImageIndex]}
alt={context.productToShow?.title}
/>
)
)}
</figure>
<div className="flex justify-around items-center">
<button
onClick={handlePrev}
className="flex justify-evenly items-center w-32 bg-black text-white font-medium py-2 rounded-lg mt-2 hover:bg-gray-900/50 transition duration-300"
>
<HiOutlineArrowNarrowLeft />
Previous
</button>
<button
onClick={handleNext}
className="flex justify-evenly items-center w-32 bg-black text-white font-medium py-2 rounded-lg mt-2 hover:bg-gray-900/50 transition duration-300"
>
Next
<HiOutlineArrowNarrowRight />
</button>
</div>
{/* // ... other code */}
<div className="p-6">
{/* <HiOutlineBadgeCheck /> */}
<h3 className="font-bold text-2xl mb-2 border-b">
{context.productToShow?.title}
</h3>
{/* table */}
<table className="table-auto w-full mt-4">
<tbody className="text-gray-700">
<tr>
<td className={tdElements}>
<HiOutlineCash /> Price
</td>
<td>${context.productToShow?.price}</td>
</tr>
<tr>
<td className={tdElements}>
<HiOutlineBadgeCheck /> Brand
</td>
<td>{context.productToShow?.brand}</td>
</tr>
<tr>
<td className={tdElements}>
<HiOutlinePhotograph />
Category
</td>
<td>{context.productToShow?.category}</td>
</tr>
<tr>
<td className={tdElements}>
<HiOutlineTag /> Discount
</td>
<td>{context.productToShow?.discountPercentage} %</td>
</tr>
<tr>
<td className={tdElements}>
<HiOutlineStar /> Rating
</td>
<td>{context.productToShow?.rating}</td>
</tr>
<tr>
<td className={tdElements}>
<HiOutlineTruck /> Stock
</td>
<td>{context.productToShow?.stock} available</td>
</tr>
{/* <tr>
<td className={tdElements}>Thumbnail</td>
<td>{context.productToShow?.thumbnail}</td>
</tr> */}
</tbody>
</table>
{/* end table */}
<div className={tdElements}>
<HiOutlineDocumentText />
<p className="mb-1 mt-2">Description</p>
</div>
<p className="text-gray-700 text-base">
{context.productToShow?.description}
</p>
<button
className="flex justify-center gap-2 items-center w-full bg-black text-white font-medium py-2 rounded-lg mt-2 hover:bg-gray-900/50 transition duration-300"
onClick={() => addProductToCart(data.data)}
>
<HiOutlineShoppingCart /> Add to Cart
</button>
</div>
{/* ... other code */}
</aside>
);
};
export default ProductDetail;
Fix the error
I am able to add products from the card as before. However, I am not able to do it from the ProductDetail
page. I am getting some errors.
Cannot read properties of undefined (reading 'id') at index.jsx:29:56 at Array.filter () at renderIcon (index.jsx:29:28) at Card (index.jsx:66:10) at renderWithHooks (react-dom.development.js:16305:18) at updateFunctionComponent (react-dom.development.js:19588:20) at beginWork (react-dom.development.js:21601:16) at HTMLUnknownElement.callCallback2 (react-dom.development.js:4164:14) at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:16) at invokeGuardedCallback (react-dom.development.js:4277:31)
36. Logout
from the Navbar
Component
For a better user experience, there is no need to make the user go to a logout page to logout if they can simply logout from the navegation menu
Here is what I change in the code:
I. Modify the Navbar
Component
Move the logout functionality to the navbar. Here’s a step-by-step plan:
- Copy the “handleLogout” function from
Logout
page and paste it in thenavbar
Component. - Attach an onClick event handler to the logout Navlink that calls the logout function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Navbar.jsx
import React from 'react';
import { useHistory } from 'react-router-dom';
const Navbar = () => {
const history = useHistory();
const handleLogout = () => {
// Add your logout logic here
console.log("You have been logged out.");
history.push('/'); // Redirect to home page after logout
};
return (
<nav>
{/* Other navbar items */}
<button onClick={handleLogout}>Logout</button>
</nav>
);
};
export default Navbar;
The navbar
should looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import { NavLink } from "react-router-dom";
import { useContext, useState } from "react";
import { AppContext } from "../../Context";
import { HiOutlineShoppingCart } from "react-icons/hi";
import { useScrollPosition } from "../../Utils/useScrollPosition";
import { useAuth } from "../../Context/auth";
import { useNavigate } from "react-router-dom";
const Navbar = () => {
const activeStyle = "underline text-gray-500 underline-offset-4";
const context = useContext(AppContext);
//scrollPosition:
const [showDropdown, setShowDropdown] = useState(false);
const [showDropdownTech, setShowDropdownTech] = useState(false);
function classNamesNavBarScroll(...classes) {
return classes.filter(Boolean).join(" ");
}
const scrollPosition = useScrollPosition();
// console.log(scrollPosition);
// AuthContext:
const auth = useAuth();
console.log("in Navbar, Auth.user: ", auth.user);
// Navigate:
let navigate = useNavigate();
// logout:
// const handleLogout = (e) => {
// e.preventDefault();
// auth.logout();
// console.log("You have been logged out.");
// navigate("/"); // Redirect to home page after logout
// };
// logout with confirmation (window.confirm)
// window.confirm() function to display a confirmation dialog when the user clicks on the logout button
const handleLogout = (e) => {
e.preventDefault();
if (
window.confirm(`${auth.user.username}, Are you sure you want to logout?`)
) {
auth.logout();
console.log("You have been logged out.");
navigate("/"); // Redirect to home page after logout
}
};
return (
<header
className={classNamesNavBarScroll(
scrollPosition > 0
? "md:shadow md:bg-white md:-translate-y-6 md:h-auto"
: "md:shadow-none md:bg-none md:translate-y-0 md:h-none",
"absolute md:fixed top-2 inset-x-0 z-40 md:transition-shadow-xl md:shadow-black md:transition-color duration-500 md:-translate-y-6 md:h-20 lg:h-14"
)}
>
<nav className="hidden sm:flex flex-col sm:flex-row justify-between items-center fixed z-10 w-full py-5 px-8 text-md font-light top-0">
<ul className="flex flex-col sm:flex-row items-center gap-3">
<li className="font-semibold text-lg">
<NavLink to="/" onClick={() => context.setSearchByCategory(null)}>
JP·Shop
</NavLink>
</li>
<li>
<NavLink
to="/"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory(null)}
>
All
</NavLink>
</li>
<li onClick={() => setShowDropdownTech(!showDropdownTech)}>
Electronics
{showDropdownTech && (
<div>
<NavLink
to="/smartphones"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
onClick={() => context.setSearchByCategory("smartphones")}
>
Phone
</NavLink>
<NavLink
to="/laptops"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
onClick={() => context.setSearchByCategory("laptops")}
>
Laptop
</NavLink>
</div>
)}
</li>
<li onClick={() => setShowDropdown(!showDropdown)}>
Cosmetics
{showDropdown && (
<div>
<NavLink
to="/fragrances"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
onClick={() => context.setSearchByCategory("fragrances")}
>
Perfumes
</NavLink>
<NavLink
to="/skincare"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
onClick={() => context.setSearchByCategory("skincare")}
>
Skin Care
</NavLink>
</div>
)}
</li>
<li>
<NavLink
to="/groceries"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory("groceries")}
>
Groceries
</NavLink>
</li>
<li>
<NavLink
to="/home-decoration"
className={({ isActive }) => (isActive ? activeStyle : undefined)}
onClick={() => context.setSearchByCategory("home-decoration")}
>
HomeGoods
</NavLink>
</li>
</ul>
<ul className="hidden sm:flex items-center gap-3">
{auth.user && (
<>
<li>
<NavLink
to="/my-account"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
My Account
</NavLink>
</li>
<li>
<NavLink
to="/my-orders"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
My Orders
</NavLink>
</li>
<li>
<NavLink
to="/card"
className={`flex justify-center items-center ${({
isActive,
}) => (isActive ? activeStyle : undefined)}`}
>
<HiOutlineShoppingCart className="mr-1" />
<p>{context.cartProducts.length}</p>
</NavLink>
</li>
<li>
<NavLink
// to="/Logout"
onClick={handleLogout}
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
Logout
</NavLink>
</li>
</>
)}
{!auth.user && (
<li>
<NavLink
to="/sign-in"
className={({ isActive }) =>
isActive ? activeStyle : undefined
}
>
Sign In
</NavLink>
</li>
)}
</ul>
</nav>
</header>
);
};
export default Navbar;
II. Delete the logout
page from the project
Before you delete, make sure to remove any references to this page from other parts of your application, such as routes or links.
III. Remove the logout
route from App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import { useRoutes, BrowserRouter, Navigate } from "react-router-dom";
import { AppProvider } from "../../Context";
import { AuthProvider } from "../../Context/auth"; // AuthContext is the context that will be used to store the user's data
import Navbar from "../../Components/Navbar";
import CheckoutSideMenu from "../../Components/CheckoutSideMenu";
import "./App.css";
import Home from "../Home";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import MyAccount from "../MyAccount";
import SignIn from "../SignIn";
// Implementing the Private and Public Routes:
import { useAuth } from "../../Context/auth"; // make sure you have a useAuth hook in your auth context
const PrivateRoute = ({ children }) => {
const { user } = useAuth();
return user ? children : <Navigate to="/sign-in" />;
};
const PublicRoute = ({ children }) => {
const { user } = useAuth();
return user ? <Navigate to="/" /> : children;
};
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/smartphones", element: <Home /> },
{ path: "/laptops", element: <Home /> },
{ path: "/fragrances", element: <Home /> },
{ path: "/skincare", element: <Home /> },
{ path: "/groceries", element: <Home /> },
{ path: "/home-decoration", element: <Home /> },
// Private Routes
{
path: "/my-order",
element: (
<PrivateRoute>
<MyOrder />
</PrivateRoute>
),
},
{
path: "/my-orders",
element: (
<PrivateRoute>
<MyOrders />
</PrivateRoute>
),
},
{
path: "/my-orders/last",
element: (
<PrivateRoute>
<MyOrder />
</PrivateRoute>
),
},
{
path: "/my-orders/:id",
element: (
<PrivateRoute>
<MyOrder />
</PrivateRoute>
),
},
{
path: "/my-account",
element: (
<PrivateRoute>
<MyAccount />
</PrivateRoute>
),
},
// Public Routes
{
path: "/sign-in",
element: (
<PublicRoute>
<SignIn />
</PublicRoute>
),
},
// Not Found
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<AppProvider>
<BrowserRouter>
<AuthProvider>
<AppRoutes />
<Navbar />
<CheckoutSideMenu />
</AuthProvider>
</BrowserRouter>
</AppProvider>
);
};
export default App;
37. Add a Skeleton
You can use the Skeleton component from shadcn to show a placeholder while your content is loading.
For this section, you will need to have Shadcn UI already setup in the project, if you havent install it, please go to step 21 in this article.
I. Install the Skeleton
Follow the steps on shadcn - Skeleton
II. Modify the home
page:
Add this section:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// ...
import { Skeleton } from "@/components/ui/skeleton"; // import the Skeleton component
// ...
const Home = () => {
const context = useContext(AppContext);
const renderView = () => {
// if there are items in the filteredItems array, render them
// Filter by title and category
// Skeleton while data is loading
if (context.isLoading) {
// if data is still loading, render the Skeleton
console.log("Rendering Skeleton");
// Create an array of 10 skeletons (or however many you expect to display)
return Array(15)
.fill()
.map((_, i) => (
<div key={i}>
{/* card: */}
<Skeleton className="w-56 h-60 rounded-lg bg-slate-200">
<Skeleton className="relative m-2 p-1 bg-slate-300/50 h-44">
<div className="flex justify-end h-36">
{/* circle: */}
<Skeleton className="h-8 w-8 rounded-full" />
</div>
{/* category: */}
<Skeleton className="absolute bottom-0 rounded-lg m-2 py-2 px-2 w-16" />
</Skeleton>
<div className="flex justify-around items-center h-10">
{/* title: */}
<Skeleton className="h-5 w-20" />
{/* price: */}
<Skeleton className="h-5 w-16" />
</div>
</Skeleton>
</div>
));
} else if (context.filteredItems?.length > 0) {
// ...
} else {
// ...
}
here is the full code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import { useContext } from "react";
import Card from "../../Components/Card";
import Layout from "../../Components/Layout";
import ProductDetail from "../../Components/ProductDetail";
import { AppContext } from "../../Context";
import { Skeleton } from "@/components/ui/skeleton"; // import the Skeleton component
const Home = () => {
const context = useContext(AppContext);
// console.log("context.isLoading: ", context.isLoading);
const renderView = () => {
// if there are items in the filteredItems array, render them
// Filter by title and category
// Skeleton while data is loading
if (context.isLoading) {
// if data is still loading, render the Skeleton
// console.log("Rendering Skeleton");
// Create an array of 10 skeletons (or however many you expect to display)
return Array(15)
.fill()
.map((_, i) => (
<div key={i}>
{/* card: */}
<Skeleton className="w-56 h-60 rounded-lg">
{/* image: */}
<Skeleton className="relative m-2 p-1 bg-slate-300/30 h-44 rounded-xl">
<div className="flex justify-end h-36">
{/* circle: */}
<Skeleton className="h-8 w-8 rounded-full" />
</div>
{/* category: */}
<Skeleton className="absolute bottom-0 rounded-lg m-2 py-2 px-2 w-16" />
</Skeleton>
<div className="flex justify-around items-center h-10">
{/* title: */}
<Skeleton className="h-5 w-20 bg-slate-200" />
{/* price: */}
<Skeleton className="h-5 w-16 bg-slate-200" />
</div>
</Skeleton>
</div>
));
} else if (context.filteredItems?.length > 0) {
return context.filteredItems?.map((item) => (
<Card key={item.id} data={item} />
));
} else {
return (
<div className="flex flex-col gap-3 w-full">
<h3 className="font-light text-md">
No results for:
<span className="font-medium text-md text-gray-400 pl-2">
{context.searchByTitle}
</span>
</h3>
<p className="font-light text-sm text-gray-400/80">
Try searching for another product
</p>
</div>
);
}
};
return (
<Layout>
<div className="flex items-center justify-center relative mb-4">
<h1 className="font-medium text-md sm:text-xl">Products</h1>
</div>
<input
type="text"
placeholder="Search a product..."
className="border border-black rounded-xl w-50 sm:w-96 px-4 py-2 mb-4"
onChange={(event) => context.setSearchByTitle(event.target.value)}
/>
<div className="grid gap-4 grid-row justify-center sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 w-full max-w-screen-sm md:max-w-screen-md lg:max-w-screen-lg xl:max-w-screen-xl">
{renderView()}
</div>
<ProductDetail />
</Layout>
);
};
export default Home;
III. Modify the Context
by Adding a Loading State
At this moment context.isLoading
is undefined.
You can add a loading state
by introducing a new state variable, isLoading
, in your AppProvider. You set isLoading
to true before you start fetching data and set it to false after you’ve fetched the data. Here’s how you can do it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import { createContext, useEffect, useState } from "react";
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
// Get Products · State to store the data from the dummy API. It's an empty array because the data is an array of objects
// Fetch data from API · hook to add the info from the API to the state
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false); // Loading state
// UseEffect is a hook to fetch the data from the API
useEffect(() => {
setIsLoading(true); // Set isLoading to true before fetching data
fetch("https://dummyjson.com/products")
.then((response) => response.json())
.then((json) => {
// console.log("Data from Dummy API: ", json); // Log the data
// console.log("Products inside Data Dummy API: ", json.products); // Products is an array of objects inside the data from the API
setItems(json.products); // Add the data to the state (setItems) and specify the data to be added (json.products)
setIsLoading(false); // Set isLoading to false after fetching data
})
.catch((error) => {
console.error("Error fetching data: ", error);
setIsLoading(false); // Set isLoading to false even if there was an error
});
}, []);
// Shopping Cart · Increment quantity
const [count, setCount] = useState(0);
// Product Detail · Open/Close
const [isProductDetailOpen, setIsProductDetailOpen] = useState(false);
const openProductDetail = () => setIsProductDetailOpen(true);
const closeProductDetail = () => setIsProductDetailOpen(false);
// Product Detail · Show product
const [productToShow, setProductToShow] = useState({});
// Shopping Cart · add product to cart
const [cartProducts, setCartProducts] = useState([]);
// Checkout Side Menu · Open/Close
const [isCheckoutSideMenuOpen, setIsCheckoutSideMenuOpen] = useState(false);
const openCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(true);
const closeCheckoutSideMenu = () => setIsCheckoutSideMenuOpen(false);
// Shopping Cart · Order
const [order, setOrder] = useState([]);
// add product to cart
const addProductToCart = (productData) => {
setCount(count + 1);
setCartProducts([...cartProducts, productData]);
// console.log("cartProducts: ", cartProducts);
// console.log("productData: ", productData);
openCheckoutSideMenu();
closeProductDetail();
};
// Get Products · Search a product
// const [searchByTitle, setSearchByTitle] = useState("");
const [searchByTitle, setSearchByTitle] = useState(null);
// console.log(searchByTitle);
// Get Products · Filter items by category
const [searchByCategory, setSearchByCategory] = useState(null);
// console.log("searchByCategory: ", searchByCategory);
// Filter items by search
const [filteredItems, setFilteredItems] = useState(null);
// Filter items by search
const filteredItemsByTitle = (items, searchByTitle) => {
return items?.filter((item) =>
item.title.toLowerCase().includes(searchByTitle.toLowerCase())
);
};
// Filter items by category
const filteredItemsByCategory = (items, searchByCategory) => {
return items?.filter((item) =>
item.category.toLowerCase().includes(searchByCategory.toLowerCase())
);
};
const filterBy = (searchType, items, searchByTitle, searchByCategory) => {
// Filter by title
if (searchType === "BY_TITLE") {
return filteredItemsByTitle(items, searchByTitle);
}
// Filter by category
if (searchType === "BY_CATEGORY") {
return filteredItemsByCategory(items, searchByCategory);
}
// Filter by title and category
if (searchType === "BY_TITLE_AND_CATEGORY") {
return filteredItemsByCategory(items, searchByCategory).filter((item) =>
item.title.toLowerCase().includes(searchByTitle.toLowerCase())
);
}
// The is NO Filter, return all items
if (!searchType) {
return items;
}
};
useEffect(() => {
// Filter by title and category
if (searchByTitle && searchByCategory) {
return setFilteredItems(
filterBy(
"BY_TITLE_AND_CATEGORY",
items,
searchByTitle,
searchByCategory
)
);
}
// Filter by title
if (searchByTitle && !searchByCategory) {
return setFilteredItems(
filterBy("BY_TITLE", items, searchByTitle, searchByCategory)
);
}
// Filter by category
if (!searchByTitle && searchByCategory) {
return setFilteredItems(
filterBy("BY_CATEGORY", items, searchByTitle, searchByCategory)
);
}
// No Filter, return all items
if (!searchByTitle && !searchByCategory) {
return setFilteredItems(
filterBy(null, items, searchByTitle, searchByCategory)
);
}
}, [items, searchByTitle, searchByCategory]);
// console.log("searchByCategory: ", searchByCategory);
// console.log("searchByTitle: ", searchByTitle);
// console.log("filteredItems: ", filteredItems);
return (
<AppContext.Provider
value=
>
{children}
</AppContext.Provider>
);
};
38. Fix error: ‘children’ is missing in props validationeslintreact/prop-types
I. Modify App
The error message is indicating that you haven’t defined prop types for the children prop in your PrivateRoute and PublicRoute components.
In React, it’s a good practice to define prop types for your components. This helps catch bugs by validating the types of props passed to your components.
You can define prop types for your PrivateRoute and PublicRoute components like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import PropTypes from "prop-types";
import { useRoutes, BrowserRouter, Navigate } from "react-router-dom";
import { AppProvider } from "../../Context";
import { AuthProvider } from "../../Context/auth"; // AuthContext is the context that will be used to store the user's data
import Navbar from "../../Components/Navbar";
import CheckoutSideMenu from "../../Components/CheckoutSideMenu";
import "./App.css";
import Home from "../Home";
import MyOrder from "../MyOrder";
import MyOrders from "../MyOrders";
import NotFound from "../NotFound";
import MyAccount from "../MyAccount";
import SignIn from "../SignIn";
// import Logout from "../Logout";
// Implementing the Private and Public Routes:
import { useAuth } from "../../Context/auth"; // make sure you have a useAuth hook in your auth context
const PrivateRoute = ({ children }) => {
const { user } = useAuth();
return user ? children : <Navigate to="/sign-in" />;
};
PrivateRoute.propTypes = {
children: PropTypes.node,
};
const PublicRoute = ({ children }) => {
const { user } = useAuth();
return user ? <Navigate to="/" /> : children;
};
PublicRoute.propTypes = {
children: PropTypes.node,
};
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/smartphones", element: <Home /> },
{ path: "/laptops", element: <Home /> },
{ path: "/fragrances", element: <Home /> },
{ path: "/skincare", element: <Home /> },
{ path: "/groceries", element: <Home /> },
{ path: "/home-decoration", element: <Home /> },
// Private Routes
{
path: "/my-order",
element: (
<PrivateRoute>
<MyOrder />
</PrivateRoute>
),
},
{
path: "/my-orders",
element: (
<PrivateRoute>
<MyOrders />
</PrivateRoute>
),
},
{
path: "/my-orders/last",
element: (
<PrivateRoute>
<MyOrder />
</PrivateRoute>
),
},
{
path: "/my-orders/:id",
element: (
<PrivateRoute>
<MyOrder />
</PrivateRoute>
),
},
{
path: "/my-account",
element: (
<PrivateRoute>
<MyAccount />
</PrivateRoute>
),
},
{
path: "/sign-in",
element: (
<PublicRoute>
<SignIn />
</PublicRoute>
),
},
// Not Found
{ path: "*", element: <NotFound /> },
]);
return routes;
};
const App = () => {
return (
<AppProvider>
<BrowserRouter>
<AuthProvider>
<AppRoutes />
<Navbar />
<CheckoutSideMenu />
</AuthProvider>
</BrowserRouter>
</AppProvider>
);
};
export default App;
II. Modify Layout
1
2
3
4
5
6
7
8
9
10
11
12
13
import PropTypes from "prop-types";
const Layout = ({ children }) => {
return (
<div className="flex flex-col items-center mt-20 mb-6">{children}</div>
);
};
export default Layout;
Layout.propTypes = {
children: PropTypes.node,
};
III. Modify Auth
The error message is indicating that you haven’t defined prop types for the children prop in your AuthProvider component.
In React, it’s a good practice to define prop types for your components. This helps catch bugs by validating the types of props passed to your components.
You can define prop types for your AuthProvider component like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import PropTypes from "prop-types";
import React from "react";
import { useNavigate } from "react-router-dom";
const AuthContext = React.createContext();
function AuthProvider({ children }) {
const navigate = useNavigate();
const [user, setUser] = React.useState(null);
const login = (username, password) => {
if (typeof username !== "string") {
throw new Error("Username must be a string");
}
setUser({ username, password });
navigate("/my-account");
};
const logout = () => {
setUser(null);
navigate("/");
};
const auth = { user, login, logout };
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}
AuthProvider.propTypes = {
children: PropTypes.node,
};
function useAuth() {
//here?
const auth = React.useContext(AuthContext);
return auth;
}
export { AuthProvider, useAuth };
39. …
I. Modify ``
1
II. Modify ``
1
III. Modify ``
1
Projects 🚀
License 📜
Distributed under the MIT License. See LICENSE.txt
for more information.
Contact 📞
Acknowledgments 📚
Resources list that I find helpful and would like to give credit to.