├── .gitignore
├── .vscode
└── extensions.json
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── server.js
└── src
├── App.js
├── App.scss
├── assets
├── forgot.png
├── loader.gif
├── login.png
├── register.png
└── spinner.jpg
├── components
├── admin
│ ├── addProduct
│ │ ├── AddProduct.js
│ │ └── AddProduct.module.scss
│ ├── changeOrderStatus
│ │ ├── ChangeOrderStatus.js
│ │ └── ChangeOrderStatus.module.scss
│ ├── home
│ │ ├── Home.js
│ │ └── Home.module.scss
│ ├── navbar
│ │ ├── Navbar.js
│ │ └── Navbar.module.scss
│ ├── orderDetails
│ │ ├── OrderDetails.js
│ │ └── OrderDetails.module.scss
│ ├── orders
│ │ ├── Orders.js
│ │ └── Orders.module.scss
│ └── viewProducts
│ │ ├── ViewProducts.js
│ │ └── ViewProducts.module.scss
├── adminOnlyRoute
│ └── AdminOnlyRoute.js
├── card
│ ├── Card.js
│ └── Card.module.scss
├── chart
│ ├── Chart.js
│ └── Chart.module.scss
├── checkoutForm
│ ├── CheckoutForm.js
│ └── CheckoutForm.module.scss
├── checkoutSummary
│ ├── CheckoutSummary.js
│ └── CheckoutSummary.module.scss
├── footer
│ ├── Footer.js
│ └── Footer.module.scss
├── header
│ ├── Header.js
│ └── Header.module.scss
├── hiddenLink
│ └── hiddenLink.js
├── index.js
├── infoBox
│ ├── InfoBox.js
│ └── InfoBox.module.scss
├── loader
│ ├── Loader.js
│ └── Loader.module.scss
├── pagination
│ ├── Pagination.js
│ └── Pagination.module.scss
├── product
│ ├── Product.js
│ ├── Product.module.scss
│ ├── productDetails
│ │ ├── ProductDetails.js
│ │ └── ProductDetails.module.scss
│ ├── productFilter
│ │ ├── ProductFilter.js
│ │ └── ProductFilter.module.scss
│ ├── productItem
│ │ ├── ProductItem.js
│ │ └── ProductItem.module.scss
│ └── productList
│ │ ├── ProductList.js
│ │ └── ProductList.module.scss
├── reviewProducts
│ ├── ReviewProducts.js
│ └── ReviewProducts.module.scss
├── search
│ ├── Search.js
│ └── Search.module.scss
├── slider
│ ├── Slider.js
│ ├── Slider.scss
│ └── slider-data.js
└── text
├── customHooks
├── useFetchCollection.js
└── useFetchDocument.js
├── firebase
└── config.js
├── index.css
├── index.js
├── pages
├── admin
│ ├── Admin.js
│ └── Admin.module.scss
├── auth
│ ├── Login.js
│ ├── Register.js
│ ├── Reset.js
│ └── auth.module.scss
├── cart
│ ├── Cart.js
│ └── Cart.module.scss
├── checkout
│ ├── Checkout.js
│ ├── CheckoutDetails.js
│ ├── CheckoutDetails.module.scss
│ └── CheckoutSuccess.js
├── contact
│ ├── Contact.js
│ └── Contact.module.scss
├── home
│ ├── Home.js
│ └── Home.module.scss
├── index.js
├── notFound
│ ├── NotFound.js
│ └── NotFound.module.scss
├── orderDetails
│ ├── OrderDetails.js
│ └── OrderDetails.module.scss
└── orderHistory
│ ├── OrderHistory.js
│ └── OrderHistory.module.scss
└── redux
├── slice
├── authSlice.js
├── cartSlice.js
├── checkoutSlice.js
├── filterSlice.js
├── orderSlice.js
└── productSlice.js
└── store.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env
26 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint"]
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starter-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emailjs/browser": "^3.6.2",
7 | "@reduxjs/toolkit": "^1.8.1",
8 | "@stripe/react-stripe-js": "^1.7.2",
9 | "@stripe/stripe-js": "^1.29.0",
10 | "@testing-library/jest-dom": "^5.16.0",
11 | "@testing-library/react": "^11.2.7",
12 | "@testing-library/user-event": "^12.8.3",
13 | "cors": "^2.8.5",
14 | "dotenv": "^16.0.0",
15 | "express": "^4.17.3",
16 | "firebase": "^9.6.11",
17 | "node-sass": "^6.0.1",
18 | "notiflix": "^3.2.5",
19 | "react": "^17.0.2",
20 | "react-chartjs-2": "^4.1.0",
21 | "react-country-region-selector": "^3.4.0",
22 | "react-dom": "^17.0.2",
23 | "react-icons": "^4.3.1",
24 | "react-redux": "^7.2.8",
25 | "react-router-dom": "^6.3.0",
26 | "react-scripts": "^5.0.0",
27 | "react-star-rate": "^0.2.0",
28 | "react-toastify": "^8.2.0",
29 | "stripe": "^8.219.0"
30 | },
31 | "scripts": {
32 | "start:frontend": "react-scripts start",
33 | "start:backend": "nodemon server.js",
34 | "start": "node server",
35 | "build": "react-scripts build",
36 | "test": "react-scripts test",
37 | "eject": "react-scripts eject"
38 | },
39 | "eslintConfig": {
40 | "extends": [
41 | "react-app",
42 | "react-app/jest"
43 | ]
44 | },
45 | "browserslist": {
46 | "production": [
47 | ">0.2%",
48 | "not dead",
49 | "not op_mini all"
50 | ],
51 | "development": [
52 | "last 1 chrome version",
53 | "last 1 firefox version",
54 | "last 1 safari version"
55 | ]
56 | },
57 | "homepage": "http://localhost:3000/"
58 | }
59 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 | Eshop App - ZinoTrust
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const express = require("express");
3 | const cors = require("cors");
4 | const stripe = require("stripe")(process.env.STRIPE_PRIVATE_KEY);
5 |
6 | const app = express();
7 | app.use(cors());
8 | app.use(express.json());
9 | const path = require("path");
10 |
11 | if (process.env.NODE_ENV === "production") {
12 | app.use(express.static("build"));
13 | app.get("*", (req, res) => {
14 | res.sendFile(path.resolve(__dirname, "build", "index.html"));
15 | });
16 | }
17 |
18 | app.get("/", (req, res) => {
19 | res.send("Welcome to eShop website.");
20 | });
21 |
22 | const array = [];
23 | const calculateOrderAmount = (items) => {
24 | items.map((item) => {
25 | const { price, cartQuantity } = item;
26 | const cartItemAmount = price * cartQuantity;
27 | return array.push(cartItemAmount);
28 | });
29 | const totalAmount = array.reduce((a, b) => {
30 | return a + b;
31 | }, 0);
32 |
33 | return totalAmount * 100;
34 | };
35 |
36 | app.post("/create-payment-intent", async (req, res) => {
37 | const { items, shipping, description } = req.body;
38 |
39 | // Create a PaymentIntent with the order amount and currency
40 | const paymentIntent = await stripe.paymentIntents.create({
41 | amount: calculateOrderAmount(items),
42 | currency: "usd",
43 | automatic_payment_methods: {
44 | enabled: true,
45 | },
46 | description,
47 | shipping: {
48 | address: {
49 | line1: shipping.line1,
50 | line2: shipping.line2,
51 | city: shipping.city,
52 | country: shipping.country,
53 | postal_code: shipping.postal_code,
54 | },
55 | name: shipping.name,
56 | phone: shipping.phone,
57 | },
58 | // receipt_email: customerEmail
59 | });
60 |
61 | res.send({
62 | clientSecret: paymentIntent.client_secret,
63 | });
64 | });
65 |
66 | const PORT = process.env.PORT || 4242;
67 | app.listen(PORT, () => console.log(`Node server listening on port ${PORT}`));
68 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Route, Routes } from "react-router-dom";
2 | import { ToastContainer } from "react-toastify";
3 | import "react-toastify/dist/ReactToastify.css";
4 | // Pages
5 | import { Home, Contact, Login, Register, Reset, Admin } from "./pages";
6 | // Components
7 | import { Header, Footer } from "./components";
8 | import AdminOnlyRoute from "./components/adminOnlyRoute/AdminOnlyRoute";
9 | import ProductDetails from "./components/product/productDetails/ProductDetails";
10 | import Cart from "./pages/cart/Cart";
11 | import CheckoutDetails from "./pages/checkout/CheckoutDetails";
12 | import Checkout from "./pages/checkout/Checkout";
13 | import CheckoutSuccess from "./pages/checkout/CheckoutSuccess";
14 | import OrderHistory from "./pages/orderHistory/OrderHistory";
15 | import OrderDetails from "./pages/orderDetails/OrderDetails";
16 | import ReviewProducts from "./components/reviewProducts/ReviewProducts";
17 | import NotFound from "./pages/notFound/NotFound";
18 |
19 | function App() {
20 | return (
21 | <>
22 |
23 |
24 |
25 |
26 | } />
27 | } />
28 | } />
29 | } />
30 | } />
31 |
32 |
36 |
37 |
38 | }
39 | />
40 |
41 | } />
42 | } />
43 | } />
44 | } />
45 | } />
46 | } />
47 | } />
48 | } />
49 | } />
50 |
51 |
52 |
53 | >
54 | );
55 | }
56 |
57 | export default App;
58 |
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/src/App.scss
--------------------------------------------------------------------------------
/src/assets/forgot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/src/assets/forgot.png
--------------------------------------------------------------------------------
/src/assets/loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/src/assets/loader.gif
--------------------------------------------------------------------------------
/src/assets/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/src/assets/login.png
--------------------------------------------------------------------------------
/src/assets/register.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/src/assets/register.png
--------------------------------------------------------------------------------
/src/assets/spinner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/src/assets/spinner.jpg
--------------------------------------------------------------------------------
/src/components/admin/addProduct/AddProduct.js:
--------------------------------------------------------------------------------
1 | import { addDoc, collection, doc, setDoc, Timestamp } from "firebase/firestore";
2 | import {
3 | deleteObject,
4 | getDownloadURL,
5 | ref,
6 | uploadBytesResumable,
7 | } from "firebase/storage";
8 | import { useState } from "react";
9 | import { useSelector } from "react-redux";
10 | import { useNavigate, useParams } from "react-router-dom";
11 | import { toast } from "react-toastify";
12 | import { db, storage } from "../../../firebase/config";
13 | import Card from "../../card/Card";
14 | import Loader from "../../loader/Loader";
15 | import styles from "./AddProduct.module.scss";
16 | import { selectProducts } from "../../../redux/slice/productSlice";
17 |
18 | const categories = [
19 | { id: 1, name: "Laptop" },
20 | { id: 2, name: "Electronics" },
21 | { id: 3, name: "Fashion" },
22 | { id: 4, name: "Phone" },
23 | ];
24 |
25 | const initialState = {
26 | name: "",
27 | imageURL: "",
28 | price: 0,
29 | category: "",
30 | brand: "",
31 | desc: "",
32 | };
33 |
34 | const AddProduct = () => {
35 | const { id } = useParams();
36 | const products = useSelector(selectProducts);
37 | const productEdit = products.find((item) => item.id === id);
38 | console.log(productEdit);
39 |
40 | const [product, setProduct] = useState(() => {
41 | const newState = detectForm(id, { ...initialState }, productEdit);
42 | return newState;
43 | });
44 |
45 | const [uploadProgress, setUploadProgress] = useState(0);
46 | const [isLoading, setIsLoading] = useState(false);
47 | const navigate = useNavigate();
48 |
49 | function detectForm(id, f1, f2) {
50 | if (id === "ADD") {
51 | return f1;
52 | }
53 | return f2;
54 | }
55 |
56 | const handleInputChange = (e) => {
57 | const { name, value } = e.target;
58 | setProduct({ ...product, [name]: value });
59 | };
60 |
61 | const handleImageChange = (e) => {
62 | const file = e.target.files[0];
63 | // console.log(file);
64 |
65 | const storageRef = ref(storage, `eshop/${Date.now()}${file.name}`);
66 | const uploadTask = uploadBytesResumable(storageRef, file);
67 |
68 | uploadTask.on(
69 | "state_changed",
70 | (snapshot) => {
71 | const progress =
72 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
73 | setUploadProgress(progress);
74 | },
75 | (error) => {
76 | toast.error(error.message);
77 | },
78 | () => {
79 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
80 | setProduct({ ...product, imageURL: downloadURL });
81 | toast.success("Image uploaded successfully.");
82 | });
83 | }
84 | );
85 | };
86 |
87 | const addProduct = (e) => {
88 | e.preventDefault();
89 | // console.log(product);
90 | setIsLoading(true);
91 |
92 | try {
93 | const docRef = addDoc(collection(db, "products"), {
94 | name: product.name,
95 | imageURL: product.imageURL,
96 | price: Number(product.price),
97 | category: product.category,
98 | brand: product.brand,
99 | desc: product.desc,
100 | createdAt: Timestamp.now().toDate(),
101 | });
102 | setIsLoading(false);
103 | setUploadProgress(0);
104 | setProduct({ ...initialState });
105 |
106 | toast.success("Product uploaded successfully.");
107 | navigate("/admin/all-products");
108 | } catch (error) {
109 | setIsLoading(false);
110 | toast.error(error.message);
111 | }
112 | };
113 |
114 | const editProduct = (e) => {
115 | e.preventDefault();
116 | setIsLoading(true);
117 |
118 | if (product.imageURL !== productEdit.imageURL) {
119 | const storageRef = ref(storage, productEdit.imageURL);
120 | deleteObject(storageRef);
121 | }
122 |
123 | try {
124 | setDoc(doc(db, "products", id), {
125 | name: product.name,
126 | imageURL: product.imageURL,
127 | price: Number(product.price),
128 | category: product.category,
129 | brand: product.brand,
130 | desc: product.desc,
131 | createdAt: productEdit.createdAt,
132 | editedAt: Timestamp.now().toDate(),
133 | });
134 | setIsLoading(false);
135 | toast.success("Product Edited Successfully");
136 | navigate("/admin/all-products");
137 | } catch (error) {
138 | setIsLoading(false);
139 | toast.error(error.message);
140 | }
141 | };
142 |
143 | return (
144 | <>
145 | {isLoading && }
146 |
147 |
{detectForm(id, "Add New Product", "Edit Product")}
148 |
149 |
247 |
248 |
249 | >
250 | );
251 | };
252 |
253 | export default AddProduct;
254 |
--------------------------------------------------------------------------------
/src/components/admin/addProduct/AddProduct.module.scss:
--------------------------------------------------------------------------------
1 | .product {
2 | .card {
3 | width: 100%;
4 | max-width: 500px;
5 | padding: 1rem;
6 | }
7 | form {
8 | label {
9 | display: block;
10 | font-size: 1.4rem;
11 | font-weight: 500;
12 | }
13 | input[type="text"],
14 | input[type="number"],
15 | input[type="file"],
16 | input[type="email"],
17 | select,
18 | textarea,
19 | input[type="password"] {
20 | display: block;
21 | font-size: 1.6rem;
22 | font-weight: 300;
23 | padding: 1rem;
24 | margin: 1rem auto;
25 | width: 100%;
26 | border: 1px solid #777;
27 | border-radius: 3px;
28 | outline: none;
29 | }
30 |
31 | // textarea {
32 | // width: 100%;
33 | // }
34 |
35 | .progress {
36 | background-color: #aaa;
37 | border: 1px solid transparent;
38 | border-radius: 10px;
39 | .progress-bar {
40 | background-color: var(--light-blue);
41 | border: 1px solid transparent;
42 | border-radius: 10px;
43 | color: #fff;
44 | font-size: 1.2rem;
45 | font-weight: 500;
46 | padding: 0 1rem;
47 | }
48 | }
49 | }
50 | }
51 |
52 | .group {
53 | border: 1px solid var(--dark-blue);
54 | padding: 5px;
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/admin/changeOrderStatus/ChangeOrderStatus.js:
--------------------------------------------------------------------------------
1 | import { collection, doc, setDoc, Timestamp } from "firebase/firestore";
2 | import React, { useState } from "react";
3 | import { useNavigate } from "react-router-dom";
4 | import { toast } from "react-toastify";
5 | import { db } from "../../../firebase/config";
6 | import Card from "../../card/Card";
7 | import Loader from "../../loader/Loader";
8 | import styles from "./ChangeOrderStatus.module.scss";
9 |
10 | const ChangeOrderStatus = ({ order, id }) => {
11 | const [status, setStatus] = useState("");
12 | const [isLoading, setIsLoading] = useState(false);
13 | const navigate = useNavigate();
14 |
15 | const editOrder = (e, id) => {
16 | e.preventDefault();
17 | setIsLoading(true);
18 |
19 | const orderConfig = {
20 | userID: order.userID,
21 | userEmail: order.userEmail,
22 | orderDate: order.orderDate,
23 | orderTime: order.orderTime,
24 | orderAmount: order.orderAmount,
25 | orderStatus: status,
26 | cartItems: order.cartItems,
27 | shippingAddress: order.shippingAddress,
28 | createdAt: order.createdAt,
29 | editedAt: Timestamp.now().toDate(),
30 | };
31 | try {
32 | setDoc(doc(db, "orders", id), orderConfig);
33 |
34 | setIsLoading(false);
35 | toast.success("Order status changes successfully");
36 | navigate("/admin/orders");
37 | } catch (error) {
38 | setIsLoading(false);
39 | toast.error(error.message);
40 | }
41 | };
42 |
43 | return (
44 | <>
45 | {isLoading && }
46 |
47 |
48 |
49 | Update Status
50 |
71 |
72 |
73 | >
74 | );
75 | };
76 |
77 | export default ChangeOrderStatus;
78 |
--------------------------------------------------------------------------------
/src/components/admin/changeOrderStatus/ChangeOrderStatus.module.scss:
--------------------------------------------------------------------------------
1 | .status {
2 | width: 100%;
3 | max-width: 400px;
4 | margin: 2rem 0;
5 | .card {
6 | padding: 1rem;
7 | border: 2px solid var(--light-blue);
8 | }
9 |
10 | form {
11 | select {
12 | display: inline-block;
13 | font-size: 1.6rem;
14 | font-weight: 300;
15 | padding: 1rem;
16 | margin: 1rem auto;
17 | width: 100%;
18 | border: 1px solid #777;
19 | border-radius: 3px;
20 | outline: none;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/admin/home/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import InfoBox from "../../infoBox/InfoBox";
3 | import styles from "./Home.module.scss";
4 | import { AiFillDollarCircle } from "react-icons/ai";
5 | import { BsCart4 } from "react-icons/bs";
6 | import { FaCartArrowDown } from "react-icons/fa";
7 | import { useDispatch, useSelector } from "react-redux";
8 | import {
9 | selectProducts,
10 | STORE_PRODUCTS,
11 | } from "../../../redux/slice/productSlice";
12 | import {
13 | CALC_TOTAL_ORDER_AMOUNT,
14 | selectOrderHistory,
15 | selectTotalOrderAmount,
16 | STORE_ORDERS,
17 | } from "../../../redux/slice/orderSlice";
18 | import useFetchCollection from "../../../customHooks/useFetchCollection";
19 | import Chart from "../../chart/Chart";
20 |
21 | //Icons
22 | const earningIcon = ;
23 | const productIcon = ;
24 | const ordersIcon = ;
25 |
26 | const Home = () => {
27 | const products = useSelector(selectProducts);
28 | const orders = useSelector(selectOrderHistory);
29 | const totalOrderAmount = useSelector(selectTotalOrderAmount);
30 |
31 | const fbProducts = useFetchCollection("products");
32 | const { data } = useFetchCollection("orders");
33 |
34 | const dispatch = useDispatch();
35 | useEffect(() => {
36 | dispatch(
37 | STORE_PRODUCTS({
38 | products: fbProducts.data,
39 | })
40 | );
41 |
42 | dispatch(STORE_ORDERS(data));
43 |
44 | dispatch(CALC_TOTAL_ORDER_AMOUNT());
45 | }, [dispatch, data, fbProducts]);
46 |
47 | return (
48 |
49 |
Admin Home
50 |
51 |
57 |
63 |
69 |
70 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default Home;
78 |
--------------------------------------------------------------------------------
/src/components/admin/home/Home.module.scss:
--------------------------------------------------------------------------------
1 | .info-box {
2 | display: flex;
3 | flex-wrap: wrap;
4 | }
5 |
6 | .card {
7 | border: 1px solid #ccc;
8 | // border-bottom: 3px solid var(--light-blue);
9 | padding: 5px;
10 | background-color: #f5f6fa;
11 | }
12 |
13 | .card1 {
14 | border-bottom: 3px solid #b624ff;
15 | }
16 | .card2 {
17 | border-bottom: 3px solid #1f93ff;
18 | }
19 | .card3 {
20 | border-bottom: 3px solid orangered;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/admin/navbar/Navbar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import { NavLink } from "react-router-dom";
4 | import { selectUserName } from "../../../redux/slice/authSlice";
5 | import styles from "./Navbar.module.scss";
6 | import { FaUserCircle } from "react-icons/fa";
7 |
8 | const activeLink = ({ isActive }) => (isActive ? `${styles.active}` : "");
9 |
10 | const Navbar = () => {
11 | const userName = useSelector(selectUserName);
12 |
13 | return (
14 |
15 |
16 |
17 |
{userName}
18 |
19 |
43 |
44 | );
45 | };
46 |
47 | export default Navbar;
48 |
--------------------------------------------------------------------------------
/src/components/admin/navbar/Navbar.module.scss:
--------------------------------------------------------------------------------
1 | .navbar {
2 | border-right: 1px solid #ccc;
3 | min-height: 80vh;
4 |
5 | .user {
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | flex-direction: column;
10 | padding: 4rem;
11 | background-color: var(--light-blue);
12 |
13 | h4 {
14 | color: #fff;
15 | }
16 | }
17 |
18 | nav ul {
19 | li {
20 | border-bottom: 1px solid #ccc;
21 | padding: 1rem;
22 | position: relative;
23 | a {
24 | display: block;
25 | width: 100%;
26 | }
27 | }
28 | }
29 | }
30 |
31 | .active {
32 | cursor: pointer;
33 | }
34 |
35 | .active::before {
36 | content: "";
37 | position: absolute;
38 | right: 0;
39 | top: 0;
40 | width: 4px;
41 | height: 100%;
42 | background-color: orangered;
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/admin/orderDetails/OrderDetails.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import useFetchDocument from "../../../customHooks/useFetchDocument";
3 | import styles from "./OrderDetails.module.scss";
4 | import spinnerImg from "../../../assets/spinner.jpg";
5 | import { Link, useParams } from "react-router-dom";
6 | import ChangeOrderStatus from "../changeOrderStatus/ChangeOrderStatus";
7 |
8 | const OrderDetails = () => {
9 | const [order, setOrder] = useState(null);
10 | const { id } = useParams();
11 | const { document } = useFetchDocument("orders", id);
12 |
13 | useEffect(() => {
14 | setOrder(document);
15 | }, [document]);
16 |
17 | return (
18 | <>
19 |
20 |
Order Details
21 |
22 | ← Back To Orders
23 |
24 |
25 | {order === null ? (
26 |

27 | ) : (
28 | <>
29 |
30 | Order ID {order.id}
31 |
32 |
33 | Order Amount ${order.orderAmount}
34 |
35 |
36 | Order Status {order.orderStatus}
37 |
38 |
39 | Shipping Address
40 |
41 | Address: {order.shippingAddress.line1},
42 | {order.shippingAddress.line2}, {order.shippingAddress.city}
43 |
44 | State: {order.shippingAddress.state}
45 |
46 | Country: {order.shippingAddress.country}
47 |
48 |
49 |
50 |
51 |
52 | s/n |
53 | Product |
54 | Price |
55 | Quantity |
56 | Total |
57 |
58 |
59 |
60 | {order.cartItems.map((cart, index) => {
61 | const { id, name, price, imageURL, cartQuantity } = cart;
62 | return (
63 |
64 |
65 | {index + 1}
66 | |
67 |
68 |
69 | {name}
70 |
71 |
76 | |
77 | {price} |
78 | {cartQuantity} |
79 | {(price * cartQuantity).toFixed(2)} |
80 |
81 | );
82 | })}
83 |
84 |
85 | >
86 | )}
87 |
88 |
89 | >
90 | );
91 | };
92 |
93 | export default OrderDetails;
94 |
--------------------------------------------------------------------------------
/src/components/admin/orderDetails/OrderDetails.module.scss:
--------------------------------------------------------------------------------
1 | .table {
2 | padding: 5px;
3 | width: 100%;
4 | overflow-x: auto;
5 |
6 | table {
7 | border-collapse: collapse;
8 | width: 100%;
9 | font-size: 1.4rem;
10 |
11 | thead {
12 | border-top: 2px solid var(--light-blue);
13 | border-bottom: 2px solid var(--light-blue);
14 | }
15 |
16 | th {
17 | border: 1px solid #eee;
18 | }
19 |
20 | th,
21 | td {
22 | vertical-align: top;
23 | text-align: left;
24 | padding: 8px;
25 | &.icons {
26 | > * {
27 | margin-right: 5px;
28 | cursor: pointer;
29 | }
30 | }
31 | }
32 |
33 | tr {
34 | border-bottom: 1px solid #ccc;
35 | }
36 |
37 | tr:nth-child(even) {
38 | background-color: #eee;
39 | }
40 | }
41 | .summary {
42 | margin-top: 2rem;
43 | display: flex;
44 | justify-content: space-between;
45 | align-items: start;
46 |
47 | .card {
48 | padding: 1rem;
49 | .text {
50 | display: flex;
51 | justify-content: space-between;
52 | align-items: center;
53 | h3 {
54 | color: var(--color-danger);
55 | }
56 | }
57 | button {
58 | margin-top: 5px;
59 | }
60 | }
61 | }
62 | }
63 | .count {
64 | display: flex;
65 | align-items: center;
66 | button {
67 | border: 1px solid var(--darkblue);
68 | }
69 | & > * {
70 | margin-right: 1rem;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/admin/orders/Orders.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { useNavigate } from "react-router-dom";
4 | import useFetchCollection from "../../../customHooks/useFetchCollection";
5 |
6 | import {
7 | selectOrderHistory,
8 | STORE_ORDERS,
9 | } from "../../../redux/slice/orderSlice";
10 | import Loader from "../../loader/Loader";
11 | import styles from "./Orders.module.scss";
12 |
13 | const Orders = () => {
14 | const { data, isLoading } = useFetchCollection("orders");
15 | const orders = useSelector(selectOrderHistory);
16 |
17 | const dispatch = useDispatch();
18 | const navigate = useNavigate();
19 |
20 | useEffect(() => {
21 | dispatch(STORE_ORDERS(data));
22 | }, [dispatch, data]);
23 |
24 | const handleClick = (id) => {
25 | navigate(`/admin/order-details/${id}`);
26 | };
27 |
28 | return (
29 | <>
30 |
31 |
Your Order History
32 |
33 | Open an order to Change order status
34 |
35 |
36 | <>
37 | {isLoading &&
}
38 |
39 | {orders.length === 0 ? (
40 |
No order found
41 | ) : (
42 |
43 |
44 |
45 | s/n |
46 | Date |
47 | Order ID |
48 | Order Amount |
49 | Order Status |
50 |
51 |
52 |
53 | {orders.map((order, index) => {
54 | const {
55 | id,
56 | orderDate,
57 | orderTime,
58 | orderAmount,
59 | orderStatus,
60 | } = order;
61 | return (
62 | handleClick(id)}>
63 | {index + 1} |
64 |
65 | {orderDate} at {orderTime}
66 | |
67 | {id} |
68 |
69 | {"$"}
70 | {orderAmount}
71 | |
72 |
73 |
80 | {orderStatus}
81 |
82 | |
83 |
84 | );
85 | })}
86 |
87 |
88 | )}
89 |
90 | >
91 |
92 | >
93 | );
94 | };
95 |
96 | export default Orders;
97 |
--------------------------------------------------------------------------------
/src/components/admin/orders/Orders.module.scss:
--------------------------------------------------------------------------------
1 | .table {
2 | padding: 5px;
3 | width: 100%;
4 | overflow-x: auto;
5 |
6 | table {
7 | border-collapse: collapse;
8 | width: 100%;
9 | font-size: 1.4rem;
10 |
11 | thead {
12 | border-top: 2px solid var(--light-blue);
13 | border-bottom: 2px solid var(--light-blue);
14 | }
15 |
16 | th {
17 | border: 1px solid #eee;
18 | }
19 |
20 | th,
21 | td {
22 | vertical-align: top;
23 | text-align: left;
24 | padding: 8px;
25 | &.icons {
26 | > * {
27 | margin-right: 5px;
28 | cursor: pointer;
29 | }
30 | }
31 | }
32 |
33 | tr {
34 | border-bottom: 1px solid #ccc;
35 | cursor: pointer;
36 | }
37 |
38 | tr:nth-child(even) {
39 | background-color: #eee;
40 | }
41 | .pending {
42 | color: orangered;
43 | font-weight: 500;
44 | }
45 | .delivered {
46 | color: green;
47 | font-weight: 500;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/admin/viewProducts/ViewProducts.js:
--------------------------------------------------------------------------------
1 | import { deleteDoc, doc } from "firebase/firestore";
2 | import { useEffect, useState } from "react";
3 | import { Link } from "react-router-dom";
4 | import { toast } from "react-toastify";
5 | import { db, storage } from "../../../firebase/config";
6 | import styles from "./ViewProducts.module.scss";
7 | import { FaEdit, FaTrashAlt } from "react-icons/fa";
8 | import Loader from "../../loader/Loader";
9 | import { deleteObject, ref } from "firebase/storage";
10 | import Notiflix from "notiflix";
11 | import { useDispatch, useSelector } from "react-redux";
12 | import {
13 | selectProducts,
14 | STORE_PRODUCTS,
15 | } from "../../../redux/slice/productSlice";
16 | import useFetchCollection from "../../../customHooks/useFetchCollection";
17 | import {
18 | FILTER_BY_SEARCH,
19 | selectFilteredProducts,
20 | } from "../../../redux/slice/filterSlice";
21 | import Search from "../../search/Search";
22 | import Pagination from "../../pagination/Pagination";
23 |
24 | const ViewProducts = () => {
25 | const [search, setSearch] = useState("");
26 | const { data, isLoading } = useFetchCollection("products");
27 | const products = useSelector(selectProducts);
28 | const filteredProducts = useSelector(selectFilteredProducts);
29 | // Pagination states
30 | const [currentPage, setCurrentPage] = useState(1);
31 | const [productsPerPage, setProductsPerPage] = useState(10);
32 | // Get Current Products
33 | const indexOfLastProduct = currentPage * productsPerPage;
34 | const indexOfFirstProduct = indexOfLastProduct - productsPerPage;
35 | const currentProducts = filteredProducts.slice(
36 | indexOfFirstProduct,
37 | indexOfLastProduct
38 | );
39 | const dispatch = useDispatch();
40 |
41 | useEffect(() => {
42 | dispatch(
43 | STORE_PRODUCTS({
44 | products: data,
45 | })
46 | );
47 | }, [dispatch, data]);
48 |
49 | useEffect(() => {
50 | dispatch(FILTER_BY_SEARCH({ products, search }));
51 | }, [dispatch, products, search]);
52 |
53 | const confirmDelete = (id, imageURL) => {
54 | Notiflix.Confirm.show(
55 | "Delete Product!!!",
56 | "You are about to delete this product",
57 | "Delete",
58 | "Cancel",
59 | function okCb() {
60 | deleteProduct(id, imageURL);
61 | },
62 | function cancelCb() {
63 | console.log("Delete Canceled");
64 | },
65 | {
66 | width: "320px",
67 | borderRadius: "3px",
68 | titleColor: "orangered",
69 | okButtonBackground: "orangered",
70 | cssAnimationStyle: "zoom",
71 | }
72 | );
73 | };
74 |
75 | const deleteProduct = async (id, imageURL) => {
76 | try {
77 | await deleteDoc(doc(db, "products", id));
78 |
79 | const storageRef = ref(storage, imageURL);
80 | await deleteObject(storageRef);
81 | toast.success("Product deleted successfully.");
82 | } catch (error) {
83 | toast.error(error.message);
84 | }
85 | };
86 |
87 | return (
88 | <>
89 | {isLoading && }
90 |
91 |
All Products
92 |
93 |
94 |
95 | {filteredProducts.length} products found
96 |
97 |
setSearch(e.target.value)} />
98 |
99 |
100 | {filteredProducts.length === 0 ? (
101 |
No product found.
102 | ) : (
103 |
104 |
105 |
106 | s/n |
107 | Image |
108 | Name |
109 | Category |
110 | Price |
111 | Actions |
112 |
113 |
114 |
115 | {currentProducts.map((product, index) => {
116 | const { id, name, price, imageURL, category } = product;
117 | return (
118 |
119 | {index + 1} |
120 |
121 |
126 | |
127 | {name} |
128 | {category} |
129 | {`$${price}`} |
130 |
131 |
132 |
133 |
134 |
135 | confirmDelete(id, imageURL)}
139 | />
140 | |
141 |
142 | );
143 | })}
144 |
145 |
146 | )}
147 |
153 |
154 | >
155 | );
156 | };
157 |
158 | export default ViewProducts;
159 |
--------------------------------------------------------------------------------
/src/components/admin/viewProducts/ViewProducts.module.scss:
--------------------------------------------------------------------------------
1 | .table {
2 | padding: 5px;
3 | width: 100%;
4 | overflow-x: auto;
5 |
6 | .search {
7 | width: 100%;
8 | max-width: 300px;
9 | }
10 |
11 | table {
12 | border-collapse: collapse;
13 | width: 100%;
14 | font-size: 1.4rem;
15 |
16 | thead {
17 | border-top: 2px solid var(--light-blue);
18 | border-bottom: 2px solid var(--light-blue);
19 | }
20 |
21 | th {
22 | border: 1px solid #eee;
23 | }
24 |
25 | th,
26 | td {
27 | vertical-align: top;
28 | text-align: left;
29 | padding: 8px;
30 | &.icons {
31 | > * {
32 | margin-right: 5px;
33 | cursor: pointer;
34 | }
35 | }
36 | }
37 |
38 | tr {
39 | border-bottom: 1px solid #ccc;
40 | }
41 |
42 | tr:nth-child(even) {
43 | background-color: #eee;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/adminOnlyRoute/AdminOnlyRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import { Link } from "react-router-dom";
4 | import { selectEmail } from "../../redux/slice/authSlice";
5 |
6 | const AdminOnlyRoute = ({ children }) => {
7 | const userEmail = useSelector(selectEmail);
8 |
9 | if (userEmail === "test@gmail.com") {
10 | return children;
11 | }
12 | return (
13 |
14 |
15 |
Permission Denied.
16 |
This page can only be view by an Admin user.
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export const AdminOnlyLink = ({ children }) => {
27 | const userEmail = useSelector(selectEmail);
28 |
29 | if (userEmail === "test@gmail.com") {
30 | return children;
31 | }
32 | return null;
33 | };
34 |
35 | export default AdminOnlyRoute;
36 |
--------------------------------------------------------------------------------
/src/components/card/Card.js:
--------------------------------------------------------------------------------
1 | import styles from "./Card.module.scss";
2 |
3 | const Card = ({ children, cardClass }) => {
4 | return {children}
;
5 | };
6 |
7 | export default Card;
8 |
--------------------------------------------------------------------------------
/src/components/card/Card.module.scss:
--------------------------------------------------------------------------------
1 | .card {
2 | border: 1px solid transparent;
3 | border-radius: 5px;
4 | box-shadow: var(--box-shadow);
5 | overflow: hidden;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/chart/Chart.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Chart as ChartJS,
4 | CategoryScale,
5 | LinearScale,
6 | BarElement,
7 | Title,
8 | Tooltip,
9 | Legend,
10 | } from "chart.js";
11 | import { Bar } from "react-chartjs-2";
12 | import Card from "../card/Card";
13 | import styles from "./Chart.module.scss";
14 | import { selectOrderHistory } from "../../redux/slice/orderSlice";
15 | import { useSelector } from "react-redux";
16 |
17 | ChartJS.register(
18 | CategoryScale,
19 | LinearScale,
20 | BarElement,
21 | Title,
22 | Tooltip,
23 | Legend
24 | );
25 |
26 | export const options = {
27 | responsive: true,
28 | plugins: {
29 | legend: {
30 | position: "top",
31 | },
32 | title: {
33 | display: false,
34 | text: "Chart.js Bar Chart",
35 | },
36 | },
37 | };
38 |
39 | const Chart = () => {
40 | const orders = useSelector(selectOrderHistory);
41 |
42 | // Create a new array of order status
43 | const array = [];
44 | orders.map((item) => {
45 | const { orderStatus } = item;
46 | return array.push(orderStatus);
47 | });
48 |
49 | const getOrderCount = (arr, value) => {
50 | return arr.filter((n) => n === value).length;
51 | };
52 |
53 | const [q1, q2, q3, q4] = [
54 | "Order Placed...",
55 | "Processing...",
56 | "Shipped...",
57 | "Delivered",
58 | ];
59 |
60 | const placed = getOrderCount(array, q1);
61 | const processing = getOrderCount(array, q2);
62 | const shipped = getOrderCount(array, q3);
63 | const delivered = getOrderCount(array, q4);
64 |
65 | const data = {
66 | labels: ["Placed Orders", "Processing", "Shipped", "Delivered"],
67 | datasets: [
68 | {
69 | label: "Order count",
70 | data: [placed, processing, shipped, delivered],
71 | backgroundColor: "rgba(255, 99, 132, 0.5)",
72 | },
73 | ],
74 | };
75 |
76 | return (
77 |
78 |
79 | Order Status Chart
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default Chart;
87 |
--------------------------------------------------------------------------------
/src/components/chart/Chart.module.scss:
--------------------------------------------------------------------------------
1 | .charts {
2 | width: 100%;
3 | max-width: 500px;
4 | .card {
5 | padding: 1rem;
6 | border: 1px solid #ccc;
7 | border-bottom: 3px solid var(--color-danger);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/checkoutForm/CheckoutForm.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | PaymentElement,
4 | useStripe,
5 | useElements,
6 | } from "@stripe/react-stripe-js";
7 | import styles from "./CheckoutForm.module.scss";
8 | import Card from "../card/Card";
9 | import CheckoutSummary from "../checkoutSummary/CheckoutSummary";
10 | import spinnerImg from "../../assets/spinner.jpg";
11 | import { toast } from "react-toastify";
12 | import { useDispatch, useSelector } from "react-redux";
13 | import { selectEmail, selectUserID } from "../../redux/slice/authSlice";
14 | import {
15 | CLEAR_CART,
16 | selectCartItems,
17 | selectCartTotalAmount,
18 | } from "../../redux/slice/cartSlice";
19 | import { selectShippingAddress } from "../../redux/slice/checkoutSlice";
20 | import { addDoc, collection, Timestamp } from "firebase/firestore";
21 | import { db } from "../../firebase/config";
22 | import { useNavigate } from "react-router-dom";
23 |
24 | const CheckoutForm = () => {
25 | const [message, setMessage] = useState(null);
26 | const [isLoading, setIsLoading] = useState(false);
27 | const stripe = useStripe();
28 | const elements = useElements();
29 |
30 | const dispatch = useDispatch();
31 | const navigate = useNavigate();
32 |
33 | const userID = useSelector(selectUserID);
34 | const userEmail = useSelector(selectEmail);
35 | const cartItems = useSelector(selectCartItems);
36 | const cartTotalAmount = useSelector(selectCartTotalAmount);
37 | const shippingAddress = useSelector(selectShippingAddress);
38 |
39 | useEffect(() => {
40 | if (!stripe) {
41 | return;
42 | }
43 |
44 | const clientSecret = new URLSearchParams(window.location.search).get(
45 | "payment_intent_client_secret"
46 | );
47 |
48 | if (!clientSecret) {
49 | return;
50 | }
51 | }, [stripe]);
52 |
53 | // Save order to Order History
54 | const saveOrder = () => {
55 | const today = new Date();
56 | const date = today.toDateString();
57 | const time = today.toLocaleTimeString();
58 | const orderConfig = {
59 | userID,
60 | userEmail,
61 | orderDate: date,
62 | orderTime: time,
63 | orderAmount: cartTotalAmount,
64 | orderStatus: "Order Placed...",
65 | cartItems,
66 | shippingAddress,
67 | createdAt: Timestamp.now().toDate(),
68 | };
69 | try {
70 | addDoc(collection(db, "orders"), orderConfig);
71 | dispatch(CLEAR_CART());
72 | toast.success("Order saved");
73 | navigate("/checkout-success");
74 | } catch (error) {
75 | toast.error(error.message);
76 | }
77 | };
78 |
79 | const handleSubmit = async (e) => {
80 | e.preventDefault();
81 | setMessage(null);
82 |
83 | if (!stripe || !elements) {
84 | return;
85 | }
86 |
87 | setIsLoading(true);
88 |
89 | const confirmPayment = await stripe
90 | .confirmPayment({
91 | elements,
92 | confirmParams: {
93 | // Make sure to change this to your payment completion page
94 | return_url: "http://localhost:3000/checkout-success",
95 | },
96 | redirect: "if_required",
97 | })
98 | .then((result) => {
99 | // ok - paymentIntent // bad - error
100 | if (result.error) {
101 | toast.error(result.error.message);
102 | setMessage(result.error.message);
103 | return;
104 | }
105 | if (result.paymentIntent) {
106 | if (result.paymentIntent.status === "succeeded") {
107 | setIsLoading(false);
108 | toast.success("Payment successful");
109 | saveOrder();
110 | }
111 | }
112 | });
113 |
114 | setIsLoading(false);
115 | };
116 |
117 | return (
118 |
119 |
120 |
Checkout
121 |
153 |
154 |
155 | );
156 | };
157 |
158 | export default CheckoutForm;
159 |
--------------------------------------------------------------------------------
/src/components/checkoutForm/CheckoutForm.module.scss:
--------------------------------------------------------------------------------
1 | .checkout {
2 | width: 100%;
3 | position: relative;
4 |
5 | .card {
6 | width: 100%;
7 | max-width: 500px;
8 | padding: 1rem;
9 | h3 {
10 | font-weight: 300;
11 | }
12 | }
13 |
14 | form {
15 | width: 100%;
16 | display: flex;
17 |
18 | div {
19 | width: 100%;
20 | }
21 |
22 | label {
23 | display: block;
24 | font-size: 1.4rem;
25 | font-weight: 500;
26 | }
27 | input[type="text"],
28 | .select,
29 | .card-details {
30 | display: block;
31 | font-size: 1.6rem;
32 | font-weight: 300;
33 | padding: 1rem;
34 | margin: 1rem auto;
35 | width: 100%;
36 | border: 1px solid #777;
37 | border-radius: 3px;
38 | outline: none;
39 | }
40 | }
41 | }
42 |
43 | @media screen and (max-width: 700px) {
44 | .checkout {
45 | form {
46 | flex-direction: column;
47 | div {
48 | width: 100%;
49 | }
50 | }
51 | }
52 | }
53 |
54 | // Payment Element styles
55 |
56 | #payment-message {
57 | color: rgb(105, 115, 134);
58 | color: red;
59 | font-size: 16px;
60 | line-height: 20px;
61 | padding-top: 12px;
62 | text-align: center;
63 | }
64 |
65 | #payment-element {
66 | margin-bottom: 24px;
67 | }
68 |
69 | /* Buttons and links */
70 | .button {
71 | background: #5469d4;
72 | font-family: Arial, sans-serif;
73 | color: #ffffff;
74 | border-radius: 4px;
75 | border: 0;
76 | padding: 12px 16px;
77 | font-size: 16px;
78 | font-weight: 600;
79 | cursor: pointer;
80 | display: block;
81 | transition: all 0.2s ease;
82 | box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
83 | width: 100%;
84 | position: relative;
85 | }
86 |
87 | .button:hover {
88 | filter: contrast(115%);
89 | }
90 |
91 | .button:disabled {
92 | opacity: 0.5;
93 | cursor: default;
94 | }
95 |
96 | // @media only screen and (max-width: 600px) {
97 | // form {
98 | // width: 80vw;
99 | // min-width: initial;
100 | // }
101 | // }
102 |
--------------------------------------------------------------------------------
/src/components/checkoutSummary/CheckoutSummary.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import { Link } from "react-router-dom";
4 | import {
5 | selectCartItems,
6 | selectCartTotalAmount,
7 | selectCartTotalQuantity,
8 | } from "../../redux/slice/cartSlice";
9 | import Card from "../card/Card";
10 | import styles from "./CheckoutSummary.module.scss";
11 |
12 | const CheckoutSummary = () => {
13 | const cartItems = useSelector(selectCartItems);
14 | const cartTotalAmount = useSelector(selectCartTotalAmount);
15 | const cartTotalQuantity = useSelector(selectCartTotalQuantity);
16 |
17 | return (
18 |
19 |
Checkout Summary
20 |
21 | {cartItems.lenght === 0 ? (
22 | <>
23 |
No item in your cart.
24 |
27 | >
28 | ) : (
29 |
30 |
31 | {`Cart item(s): ${cartTotalQuantity}`}
32 |
33 |
34 |
Subtotal:
35 | {cartTotalAmount.toFixed(2)}
36 |
37 | {cartItems.map((item, index) => {
38 | const { id, name, price, cartQuantity } = item;
39 | return (
40 |
41 | Product: {name}
42 | Quantity: {cartQuantity}
43 | Unit price: {price}
44 | Set price: {price * cartQuantity}
45 |
46 | );
47 | })}
48 |
49 | )}
50 |
51 |
52 | );
53 | };
54 |
55 | export default CheckoutSummary;
56 |
--------------------------------------------------------------------------------
/src/components/checkoutSummary/CheckoutSummary.module.scss:
--------------------------------------------------------------------------------
1 | .card {
2 | width: 100%;
3 | max-width: 500px;
4 | padding: 1rem;
5 | border: 1px solid var(--light-blue);
6 | margin-bottom: 5px;
7 | }
8 | h3 {
9 | font-weight: 300;
10 | }
11 |
12 | .text {
13 | display: flex;
14 | justify-content: space-between;
15 | align-items: center;
16 | h3 {
17 | color: var(--color-danger);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Footer.module.scss";
3 |
4 | const date = new Date();
5 | const year = date.getFullYear();
6 |
7 | const Footer = () => {
8 | return © {year} All Rights Reserved
;
9 | };
10 |
11 | export default Footer;
12 |
--------------------------------------------------------------------------------
/src/components/footer/Footer.module.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | background-color: var(--dark-blue);
3 | color: #fff;
4 | font-size: 1.6rem;
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | height: 8rem;
9 | p {
10 | color: #fff;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/header/Header.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link, NavLink, useNavigate } from "react-router-dom";
3 | import styles from "./Header.module.scss";
4 | import { FaShoppingCart, FaTimes, FaUserCircle } from "react-icons/fa";
5 | import { HiOutlineMenuAlt3 } from "react-icons/hi";
6 | import { auth } from "../../firebase/config";
7 | import { onAuthStateChanged, signOut } from "firebase/auth";
8 | import { toast } from "react-toastify";
9 | import { useEffect } from "react";
10 | import { useDispatch, useSelector } from "react-redux";
11 | import {
12 | REMOVE_ACTIVE_USER,
13 | SET_ACTIVE_USER,
14 | } from "../../redux/slice/authSlice";
15 | import ShowOnLogin, { ShowOnLogout } from "../hiddenLink/hiddenLink";
16 | import { AdminOnlyLink } from "../adminOnlyRoute/AdminOnlyRoute";
17 | import {
18 | CALCULATE_TOTAL_QUANTITY,
19 | selectCartTotalQuantity,
20 | } from "../../redux/slice/cartSlice";
21 |
22 | const logo = (
23 |
24 |
25 |
26 | eShop.
27 |
28 |
29 |
30 | );
31 |
32 | const activeLink = ({ isActive }) => (isActive ? `${styles.active}` : "");
33 |
34 | const Header = () => {
35 | const [showMenu, setShowMenu] = useState(false);
36 | const [displayName, setdisplayName] = useState("");
37 | const [scrollPage, setScrollPage] = useState(false);
38 | const cartTotalQuantity = useSelector(selectCartTotalQuantity);
39 |
40 | useEffect(() => {
41 | dispatch(CALCULATE_TOTAL_QUANTITY());
42 | }, []);
43 |
44 | const navigate = useNavigate();
45 |
46 | const dispatch = useDispatch();
47 |
48 | const fixNavbar = () => {
49 | if (window.scrollY > 50) {
50 | setScrollPage(true);
51 | } else {
52 | setScrollPage(false);
53 | }
54 | };
55 | window.addEventListener("scroll", fixNavbar);
56 |
57 | // Monitor currently sign in user
58 | useEffect(() => {
59 | onAuthStateChanged(auth, (user) => {
60 | if (user) {
61 | // console.log(user);
62 | if (user.displayName == null) {
63 | const u1 = user.email.slice(0, -10);
64 | const uName = u1.charAt(0).toUpperCase() + u1.slice(1);
65 | setdisplayName(uName);
66 | } else {
67 | setdisplayName(user.displayName);
68 | }
69 |
70 | dispatch(
71 | SET_ACTIVE_USER({
72 | email: user.email,
73 | userName: user.displayName ? user.displayName : displayName,
74 | userID: user.uid,
75 | })
76 | );
77 | } else {
78 | setdisplayName("");
79 | dispatch(REMOVE_ACTIVE_USER());
80 | }
81 | });
82 | }, [dispatch, displayName]);
83 |
84 | const toggleMenu = () => {
85 | setShowMenu(!showMenu);
86 | };
87 |
88 | const hideMenu = () => {
89 | setShowMenu(false);
90 | };
91 |
92 | const logoutUser = () => {
93 | signOut(auth)
94 | .then(() => {
95 | toast.success("Logout successfully.");
96 | navigate("/");
97 | })
98 | .catch((error) => {
99 | toast.error(error.message);
100 | });
101 | };
102 |
103 | const cart = (
104 |
105 |
106 | Cart
107 |
108 | {cartTotalQuantity}
109 |
110 |
111 | );
112 |
113 | return (
114 | <>
115 |
116 |
117 | {logo}
118 |
119 |
183 |
184 |
185 | {cart}
186 |
187 |
188 |
189 |
190 | >
191 | );
192 | };
193 |
194 | export default Header;
195 |
--------------------------------------------------------------------------------
/src/components/header/Header.module.scss:
--------------------------------------------------------------------------------
1 | .fixed {
2 | width: 100%;
3 | position: fixed;
4 | top: 0;
5 | transition: all 0.5s;
6 | z-index: 9;
7 | }
8 |
9 | header {
10 | width: 100%;
11 | background-color: var(--dark-blue);
12 | color: #fff;
13 |
14 | .header {
15 | width: 100%;
16 | height: 8rem;
17 | max-width: 1000px;
18 | margin: 0 auto;
19 | padding: 1rem;
20 | display: flex;
21 | justify-content: space-between;
22 | align-items: center;
23 | position: relative;
24 | }
25 |
26 | .logo a h2 {
27 | width: 25%;
28 | color: #fff;
29 | cursor: pointer;
30 | span {
31 | color: orangered;
32 | }
33 | }
34 |
35 | nav {
36 | width: 75%;
37 | display: flex;
38 | justify-content: space-between;
39 | ul {
40 | display: flex;
41 | justify-content: space-between;
42 | list-style: none;
43 |
44 | .logo-mobile {
45 | display: none;
46 | }
47 |
48 | li {
49 | margin: 0 5px;
50 | a {
51 | color: #fff;
52 | &:hover {
53 | color: orangered;
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
60 | .header-right {
61 | display: flex;
62 |
63 | .cart a {
64 | display: flex;
65 | color: #fff;
66 | position: relative;
67 | &:hover {
68 | color: orangered;
69 | }
70 | &.active {
71 | color: var(--color-danger);
72 | }
73 | p {
74 | position: absolute;
75 | top: -1rem;
76 | right: -1rem;
77 | font-weight: 500;
78 | }
79 | }
80 |
81 | span {
82 | margin: 0 5px;
83 |
84 | p {
85 | color: #fff;
86 | }
87 | }
88 |
89 | .links a {
90 | margin: 0 5px;
91 | color: #fff;
92 | &:hover {
93 | color: orangered;
94 | }
95 | &.active {
96 | color: var(--color-danger);
97 | }
98 | }
99 | }
100 |
101 | .menu-icon {
102 | cursor: pointer;
103 | display: none;
104 | }
105 |
106 | @media screen and (max-width: 800px) {
107 | nav {
108 | display: block;
109 | position: absolute;
110 | top: 0;
111 | left: 0;
112 | width: 50%;
113 | height: 100vh;
114 | background-color: var(--dark-blue);
115 | padding: 1rem;
116 | z-index: 999;
117 | transform: translateX(-200%);
118 | transition: all 0.3s;
119 |
120 | .nav-wrapper {
121 | position: absolute;
122 | top: 0;
123 | right: 0;
124 | width: 100%;
125 | height: 100vh;
126 | background-color: rgba(0, 0, 0, 0.5);
127 | transform: translateX();
128 | transition: all 0.3s;
129 | }
130 |
131 | .show-nav-wrapper {
132 | transform: translateX(100%);
133 | }
134 |
135 | ul {
136 | display: block;
137 | .logo-mobile {
138 | display: flex;
139 | justify-content: space-between;
140 | align-items: center;
141 | > * {
142 | cursor: pointer;
143 | }
144 | }
145 | li {
146 | padding: 5px 0;
147 | border-bottom: 1px solid #333;
148 | a {
149 | display: block;
150 | }
151 | }
152 | }
153 |
154 | .header-right {
155 | display: block;
156 | .cart {
157 | // display: block;
158 | // padding: 5px 0;
159 | border-bottom: 1px solid #333;
160 | // a {
161 | // position: relative;
162 | // p {
163 | // position: absolute;
164 | // top: -1rem;
165 | // left: 5rem;
166 | // font-weight: 500;
167 | // }
168 | // }
169 | }
170 |
171 | .links {
172 | display: block;
173 | a {
174 | display: block;
175 | margin: 0;
176 | padding: 5px 0;
177 | border-bottom: 1px solid #333;
178 | }
179 | }
180 | }
181 | }
182 | .cart {
183 | display: block;
184 | padding: 5px 0;
185 | // border-bottom: 1px solid #333;
186 | a {
187 | color: #fff;
188 | position: relative;
189 | &:hover {
190 | color: orangered;
191 | }
192 | p {
193 | position: absolute;
194 | top: -1rem;
195 | left: 5rem;
196 | font-weight: 500;
197 | color: #fff;
198 | }
199 | }
200 | }
201 | .show-nav {
202 | transform: translateX(0);
203 | }
204 | .hide-nav {
205 | transform: translateX(-200%);
206 | }
207 | .menu-icon {
208 | display: flex;
209 | > * {
210 | margin-left: 2rem;
211 | }
212 | }
213 | }
214 | }
215 |
216 | .active {
217 | position: relative;
218 | color: var(--color-danger);
219 | }
220 |
221 | .active::after {
222 | content: "";
223 | position: absolute;
224 | left: 0;
225 | bottom: -3px;
226 | width: 100%;
227 | height: 2px;
228 | background-color: #fff;
229 | }
230 |
--------------------------------------------------------------------------------
/src/components/hiddenLink/hiddenLink.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectIsLoggedIn } from "../../redux/slice/authSlice";
3 |
4 | const ShowOnLogin = ({ children }) => {
5 | const isLoggedIn = useSelector(selectIsLoggedIn);
6 |
7 | if (isLoggedIn) {
8 | return children;
9 | }
10 | return null;
11 | };
12 |
13 | export const ShowOnLogout = ({ children }) => {
14 | const isLoggedIn = useSelector(selectIsLoggedIn);
15 |
16 | if (!isLoggedIn) {
17 | return children;
18 | }
19 | return null;
20 | };
21 |
22 | export default ShowOnLogin;
23 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Header } from "./header/Header";
2 | export { default as Footer } from "./footer/Footer";
3 |
--------------------------------------------------------------------------------
/src/components/infoBox/InfoBox.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Card from "../card/Card";
3 | import styles from "./InfoBox.module.scss";
4 |
5 | const InfoBox = ({ cardClass, title, count, icon }) => {
6 | return (
7 |
8 |
9 | {title}
10 |
11 | {count}
12 | {icon}
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default InfoBox;
20 |
--------------------------------------------------------------------------------
/src/components/infoBox/InfoBox.module.scss:
--------------------------------------------------------------------------------
1 | .info-box {
2 | width: 100%;
3 | max-width: 25rem;
4 | margin-right: 1rem;
5 | margin-bottom: 1rem;
6 |
7 | .card {
8 | border: 1px solid #ccc;
9 | border-bottom: 3px solid var(--light-blue);
10 | padding: 5px;
11 | background-color: #f5f6fa;
12 | }
13 |
14 | span {
15 | display: flex;
16 | justify-content: space-between;
17 | align-items: center;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/loader/Loader.js:
--------------------------------------------------------------------------------
1 | import styles from "./Loader.module.scss";
2 | import loaderImg from "../../assets/loader.gif";
3 | import ReactDOM from "react-dom";
4 |
5 | const Loader = () => {
6 | return ReactDOM.createPortal(
7 |
8 |
9 |

10 |
11 |
,
12 | document.getElementById("loader")
13 | );
14 | };
15 |
16 | export default Loader;
17 |
--------------------------------------------------------------------------------
/src/components/loader/Loader.module.scss:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: fixed;
3 | width: 100vw;
4 | height: 100vh;
5 | background-color: rgba(0, 0, 0, 0.7);
6 | z-index: 9;
7 | }
8 |
9 | .loader {
10 | position: fixed;
11 | left: 50%;
12 | top: 50%;
13 | transform: translate(-50%, -50%);
14 | z-index: 999;
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/pagination/Pagination.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styles from "./Pagination.module.scss";
3 |
4 | const Pagination = ({
5 | currentPage,
6 | setCurrentPage,
7 | productsPerPage,
8 | totalProducts,
9 | }) => {
10 | const pageNumbers = [];
11 | const totalPages = totalProducts / productsPerPage;
12 | // Limit the page Numbers shown
13 | const [pageNumberLimit] = useState(5);
14 | const [maxPageNumberLimit, setmaxPageNumberLimit] = useState(5);
15 | const [minPageNumberLimit, setminPageNumberLimit] = useState(0);
16 |
17 | // Paginate
18 | const paginate = (pageNumber) => {
19 | setCurrentPage(pageNumber);
20 | };
21 |
22 | // GO to next page
23 | const paginateNext = () => {
24 | setCurrentPage(currentPage + 1);
25 | // Show next set of pageNumbers
26 | if (currentPage + 1 > maxPageNumberLimit) {
27 | setmaxPageNumberLimit(maxPageNumberLimit + pageNumberLimit);
28 | setminPageNumberLimit(minPageNumberLimit + pageNumberLimit);
29 | }
30 | };
31 |
32 | // GO to prev page
33 | const paginatePrev = () => {
34 | setCurrentPage(currentPage - 1);
35 | // Show prev set of pageNumbers
36 | if ((currentPage - 1) % pageNumberLimit === 0) {
37 | setmaxPageNumberLimit(maxPageNumberLimit - pageNumberLimit);
38 | setminPageNumberLimit(minPageNumberLimit - pageNumberLimit);
39 | }
40 | };
41 |
42 | for (let i = 1; i <= Math.ceil(totalProducts / productsPerPage); i++) {
43 | pageNumbers.push(i);
44 | }
45 | // console.log(pageNumbers);
46 |
47 | return (
48 |
87 | );
88 | };
89 |
90 | export default Pagination;
91 |
--------------------------------------------------------------------------------
/src/components/pagination/Pagination.module.scss:
--------------------------------------------------------------------------------
1 | .pagination {
2 | list-style: none;
3 | margin-top: 1rem;
4 | padding-top: 1rem;
5 | border-top: 2px solid #ccc;
6 |
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 |
11 | .hidden {
12 | display: none;
13 | }
14 |
15 | li {
16 | font-size: 1.4rem;
17 | border: 1px solid #ccc;
18 | min-width: 3rem;
19 | height: 3rem;
20 | padding: 3px;
21 | display: flex;
22 | justify-content: center;
23 | align-items: center;
24 | cursor: pointer;
25 | }
26 | p {
27 | margin-left: 1rem;
28 | .page {
29 | color: var(--color-danger);
30 | }
31 | }
32 | }
33 | .active {
34 | background-color: var(--color-danger);
35 | color: #fff;
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/product/Product.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import useFetchCollection from "../../customHooks/useFetchCollection";
4 | import {
5 | GET_PRICE_RANGE,
6 | selectProducts,
7 | STORE_PRODUCTS,
8 | } from "../../redux/slice/productSlice";
9 | import styles from "./Product.module.scss";
10 | import ProductFilter from "./productFilter/ProductFilter";
11 | import ProductList from "./productList/ProductList";
12 | import spinnerImg from "../../assets/spinner.jpg";
13 | import { FaCogs } from "react-icons/fa";
14 |
15 | const Product = () => {
16 | const { data, isLoading } = useFetchCollection("products");
17 | const [showFilter, setShowFilter] = useState(false);
18 | const products = useSelector(selectProducts);
19 | const dispatch = useDispatch();
20 |
21 | useEffect(() => {
22 | dispatch(
23 | STORE_PRODUCTS({
24 | products: data,
25 | })
26 | );
27 |
28 | dispatch(
29 | GET_PRICE_RANGE({
30 | products: data,
31 | })
32 | );
33 | }, [dispatch, data]);
34 |
35 | const toggleFilter = () => {
36 | setShowFilter(!showFilter);
37 | };
38 |
39 | return (
40 |
41 |
42 |
49 |
50 | {isLoading ? (
51 |

57 | ) : (
58 |
59 | )}
60 |
61 |
62 |
63 | {showFilter ? "Hide Filter" : "Show Filter"}
64 |
65 |
66 |
67 |
68 |
69 | );
70 | };
71 |
72 | export default Product;
73 |
--------------------------------------------------------------------------------
/src/components/product/Product.module.scss:
--------------------------------------------------------------------------------
1 | .product {
2 | display: flex;
3 | position: relative;
4 |
5 | .filter {
6 | width: 20%;
7 | // border: 2px solid var(--dark-blue);
8 | transition: all 0.3s;
9 | }
10 |
11 | .content {
12 | width: 80%;
13 | padding-left: 5px;
14 | position: relative;
15 | .icon {
16 | display: none;
17 | justify-content: center;
18 | align-items: center;
19 | position: absolute;
20 | right: 0;
21 | top: 0;
22 | cursor: pointer;
23 |
24 | & > * {
25 | padding-left: 5px;
26 | }
27 | }
28 | }
29 | @media screen and (max-width: 700px) {
30 | .filter {
31 | width: 50%;
32 | background-color: #fff;
33 | border: 2px solid var(--dark-blue);
34 | position: absolute;
35 | left: -200%;
36 | height: 100%;
37 | padding: 1rem;
38 | z-index: 99;
39 | }
40 | .show {
41 | left: 0;
42 | }
43 | .content {
44 | width: 100%;
45 | .icon {
46 | display: flex;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/product/productDetails/ProductDetails.js:
--------------------------------------------------------------------------------
1 | import styles from "./ProductDetails.module.scss";
2 |
3 | import React, { useEffect, useState } from "react";
4 | import { Link, useParams } from "react-router-dom";
5 |
6 | import spinnerImg from "../../../assets/spinner.jpg";
7 |
8 | import { useDispatch, useSelector } from "react-redux";
9 | import {
10 | ADD_TO_CART,
11 | CALCULATE_TOTAL_QUANTITY,
12 | DECREASE_CART,
13 | selectCartItems,
14 | } from "../../../redux/slice/cartSlice";
15 | import useFetchDocument from "../../../customHooks/useFetchDocument";
16 | import useFetchCollection from "../../../customHooks/useFetchCollection";
17 | import Card from "../../card/Card";
18 | import StarsRating from "react-star-rate";
19 |
20 | const ProductDetails = () => {
21 | const { id } = useParams();
22 | const [product, setProduct] = useState(null);
23 | const dispatch = useDispatch();
24 | const cartItems = useSelector(selectCartItems);
25 | const { document } = useFetchDocument("products", id);
26 | const { data } = useFetchCollection("reviews");
27 | const filteredReviews = data.filter((review) => review.productID === id);
28 |
29 | const cart = cartItems.find((cart) => cart.id === id);
30 | const isCartAdded = cartItems.findIndex((cart) => {
31 | return cart.id === id;
32 | });
33 |
34 | useEffect(() => {
35 | setProduct(document);
36 | }, [document]);
37 |
38 | const addToCart = (product) => {
39 | dispatch(ADD_TO_CART(product));
40 | dispatch(CALCULATE_TOTAL_QUANTITY());
41 | };
42 |
43 | const decreaseCart = (product) => {
44 | dispatch(DECREASE_CART(product));
45 | dispatch(CALCULATE_TOTAL_QUANTITY());
46 | };
47 |
48 | return (
49 |
50 |
51 |
Product Details
52 |
53 | ← Back To Products
54 |
55 | {product === null ? (
56 |

57 | ) : (
58 | <>
59 |
60 |
61 |

62 |
63 |
64 |
{product.name}
65 |
{`$${product.price}`}
66 |
{product.desc}
67 |
68 | SKU {product.id}
69 |
70 |
71 | Brand {product.brand}
72 |
73 |
74 |
75 | {isCartAdded < 0 ? null : (
76 | <>
77 |
83 |
84 | {cart.cartQuantity}
85 |
86 |
92 | >
93 | )}
94 |
95 |
101 |
102 |
103 | >
104 | )}
105 |
106 | Product Reviews
107 |
108 | {filteredReviews.length === 0 ? (
109 |
There are no reviews for this product yet.
110 | ) : (
111 | <>
112 | {filteredReviews.map((item, index) => {
113 | const { rate, review, reviewDate, userName } = item;
114 | return (
115 |
116 |
117 |
{review}
118 |
119 | {reviewDate}
120 |
121 |
122 |
123 | by: {userName}
124 |
125 |
126 | );
127 | })}
128 | >
129 | )}
130 |
131 |
132 |
133 |
134 | );
135 | };
136 |
137 | export default ProductDetails;
138 |
--------------------------------------------------------------------------------
/src/components/product/productDetails/ProductDetails.module.scss:
--------------------------------------------------------------------------------
1 | .product {
2 | .card {
3 | padding: 1rem;
4 | margin-top: 1rem;
5 | }
6 | .review {
7 | border-top: 1px solid #ccc;
8 | }
9 | .details {
10 | padding-top: 2rem;
11 | display: flex;
12 |
13 | .img {
14 | width: 45%;
15 | border: 1px solid #ccc;
16 | border-radius: 3px;
17 | img {
18 | width: 100%;
19 | }
20 | }
21 |
22 | .content {
23 | width: 55%;
24 | padding: 0 5px;
25 | & > * {
26 | margin-bottom: 1rem;
27 | }
28 | .price {
29 | color: orangered;
30 | font-weight: 500;
31 | }
32 |
33 | .count {
34 | display: flex;
35 | align-items: center;
36 | & > * {
37 | margin-right: 1rem;
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
44 | @media screen and (max-width: 700px) {
45 | .product {
46 | .details {
47 | flex-direction: column;
48 |
49 | .img {
50 | width: 100%;
51 | }
52 |
53 | .content {
54 | width: 100%;
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/product/productFilter/ProductFilter.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import {
4 | FILTER_BY_BRAND,
5 | FILTER_BY_CATEGORY,
6 | FILTER_BY_PRICE,
7 | } from "../../../redux/slice/filterSlice";
8 | import {
9 | selectMaxPrice,
10 | selectMinPrice,
11 | selectProducts,
12 | } from "../../../redux/slice/productSlice";
13 | import styles from "./ProductFilter.module.scss";
14 |
15 | const ProductFilter = () => {
16 | const [category, setCategory] = useState("All");
17 | const [brand, setBrand] = useState("All");
18 | const [price, setPrice] = useState(3000);
19 | const products = useSelector(selectProducts);
20 | const minPrice = useSelector(selectMinPrice);
21 | const maxPrice = useSelector(selectMaxPrice);
22 |
23 | const dispatch = useDispatch();
24 |
25 | const allCategories = [
26 | "All",
27 | ...new Set(products.map((product) => product.category)),
28 | ];
29 | const allBrands = [
30 | "All",
31 | ...new Set(products.map((product) => product.brand)),
32 | ];
33 | // console.log(allBrands);
34 |
35 | useEffect(() => {
36 | dispatch(FILTER_BY_BRAND({ products, brand }));
37 | }, [dispatch, products, brand]);
38 |
39 | useEffect(() => {
40 | dispatch(FILTER_BY_PRICE({ products, price }));
41 | }, [dispatch, products, price]);
42 |
43 | const filterProducts = (cat) => {
44 | setCategory(cat);
45 | dispatch(FILTER_BY_CATEGORY({ products, category: cat }));
46 | };
47 |
48 | const clearFilters = () => {
49 | setCategory("All");
50 | setBrand("All");
51 | setPrice(maxPrice);
52 | };
53 |
54 | return (
55 |
56 |
Categories
57 |
58 | {allCategories.map((cat, index) => {
59 | return (
60 |
68 | );
69 | })}
70 |
71 |
Brand
72 |
73 |
82 |
Price
83 |
{`$${price}`}
84 |
85 | setPrice(e.target.value)}
89 | min={minPrice}
90 | max={maxPrice}
91 | />
92 |
93 |
94 |
97 |
98 |
99 | );
100 | };
101 |
102 | export default ProductFilter;
103 |
--------------------------------------------------------------------------------
/src/components/product/productFilter/ProductFilter.module.scss:
--------------------------------------------------------------------------------
1 | .filter {
2 | h4 {
3 | margin-top: 1rem;
4 | }
5 | .category button {
6 | display: block;
7 | text-align: left;
8 | width: 80%;
9 | height: 3rem;
10 | font-size: 1.5rem;
11 | border: none;
12 | background-color: transparent;
13 | cursor: pointer;
14 | // margin-bottom: 5px;
15 | border-bottom: 1px solid #777;
16 | }
17 |
18 | .brand {
19 | select {
20 | font-size: 1.6rem;
21 | font-weight: 300;
22 | padding: 5px;
23 |
24 | width: 80%;
25 | border: 1px solid #777;
26 | border-radius: 3px;
27 | outline: none;
28 | }
29 | }
30 | }
31 |
32 | @media screen and (max-width: 700px) {
33 | .filter {
34 | .category button {
35 | width: 100%;
36 | }
37 | .brand {
38 | select {
39 | width: 100%;
40 | }
41 | }
42 | }
43 | }
44 |
45 | .active {
46 | position: relative;
47 | // margin-bottom: 2px;
48 | padding-left: 1rem;
49 | }
50 |
51 | .active::before {
52 | content: "";
53 | position: absolute;
54 | left: 0;
55 | bottom: 0;
56 | width: 2px;
57 | height: 100%;
58 | background-color: var(--color-danger);
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/product/productItem/ProductItem.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch } from "react-redux";
3 | import { Link } from "react-router-dom";
4 | import {
5 | ADD_TO_CART,
6 | CALCULATE_TOTAL_QUANTITY,
7 | } from "../../../redux/slice/cartSlice";
8 | import Card from "../../card/Card";
9 | import styles from "./ProductItem.module.scss";
10 |
11 | const ProductItem = ({ product, grid, id, name, price, desc, imageURL }) => {
12 | const dispatch = useDispatch();
13 | const shortenText = (text, n) => {
14 | if (text.length > n) {
15 | const shortenedText = text.substring(0, n).concat("...");
16 | return shortenedText;
17 | }
18 | return text;
19 | };
20 |
21 | const addToCart = (product) => {
22 | dispatch(ADD_TO_CART(product));
23 | dispatch(CALCULATE_TOTAL_QUANTITY());
24 | };
25 |
26 | return (
27 |
28 |
29 |
30 |

31 |
32 |
33 |
34 |
35 |
{`$${price}`}
36 |
{shortenText(name, 18)}
37 |
38 | {!grid &&
{shortenText(desc, 200)}
}
39 |
40 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default ProductItem;
52 |
--------------------------------------------------------------------------------
/src/components/product/productItem/ProductItem.module.scss:
--------------------------------------------------------------------------------
1 | .grid {
2 | width: 24rem;
3 | background-color: #fff;
4 | margin: 5px;
5 | height: 36rem;
6 | position: relative;
7 | .img {
8 | padding: 1rem;
9 | width: 100%;
10 | max-height: 75%;
11 | overflow: hidden;
12 | border-bottom: 2px solid #eee;
13 | img {
14 | width: 100%;
15 | max-width: 100%;
16 | // height: 100%;
17 | // max-height: 100%;
18 | cursor: pointer;
19 | }
20 | }
21 | .content {
22 | text-align: center;
23 | position: absolute;
24 | bottom: 0;
25 | left: 0;
26 | width: 100%;
27 |
28 | .details {
29 | // display: flex;
30 | // justify-content: space-between;
31 | // align-items: center;
32 | padding: 0 1rem;
33 | h4 {
34 | font-weight: 400;
35 | font-size: 1.8rem;
36 | }
37 | p {
38 | font-weight: 500;
39 | color: orangered;
40 | }
41 | }
42 | button {
43 | display: block;
44 | width: 100%;
45 | }
46 | }
47 | }
48 |
49 | .list {
50 | width: 100%;
51 | height: 28rem;
52 | max-height: 32rem;
53 | display: flex;
54 | background-color: #fff;
55 | margin: 1rem 0;
56 | .img {
57 | padding: 1rem;
58 | width: 100%;
59 | // max-width: 35%;
60 | height: 100%;
61 | overflow: hidden;
62 | border-right: 2px solid #eee;
63 | // border: 1px solid red;
64 | img {
65 | width: 100%;
66 | // height: 100%;
67 | max-height: 100%;
68 | cursor: pointer;
69 | }
70 | }
71 |
72 | .content {
73 | position: relative;
74 | padding: 1rem;
75 | width: 65%;
76 | .details {
77 | display: flex;
78 | flex-direction: column;
79 | margin-bottom: 1rem;
80 | h4 {
81 | font-weight: 400;
82 | }
83 | p {
84 | font-weight: 500;
85 | color: var(--color-danger);
86 | }
87 | }
88 | button {
89 | position: absolute;
90 | bottom: 1rem;
91 | left: 1rem;
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/components/product/productList/ProductList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import styles from "./ProductList.module.scss";
3 | import { BsFillGridFill } from "react-icons/bs";
4 | import { FaListAlt } from "react-icons/fa";
5 | import Search from "../../search/Search";
6 | import ProductItem from "../productItem/ProductItem";
7 | import { useDispatch, useSelector } from "react-redux";
8 | import {
9 | FILTER_BY_SEARCH,
10 | selectFilteredProducts,
11 | SORT_PRODUCTS,
12 | } from "../../../redux/slice/filterSlice";
13 | import Pagination from "../../pagination/Pagination";
14 |
15 | const ProductList = ({ products }) => {
16 | const [grid, setGrid] = useState(true);
17 | const [search, setSearch] = useState("");
18 | const [sort, setSort] = useState("latest");
19 | const filteredProducts = useSelector(selectFilteredProducts);
20 |
21 | // Pagination states
22 | const [currentPage, setCurrentPage] = useState(1);
23 | const [productsPerPage] = useState(9);
24 | // Get Current Products
25 | const indexOfLastProduct = currentPage * productsPerPage;
26 | const indexOfFirstProduct = indexOfLastProduct - productsPerPage;
27 | const currentProducts = filteredProducts.slice(
28 | indexOfFirstProduct,
29 | indexOfLastProduct
30 | );
31 |
32 | const dispatch = useDispatch();
33 |
34 | useEffect(() => {
35 | dispatch(SORT_PRODUCTS({ products, sort }));
36 | }, [dispatch, products, sort]);
37 |
38 | useEffect(() => {
39 | dispatch(FILTER_BY_SEARCH({ products, search }));
40 | }, [dispatch, products, search]);
41 |
42 | return (
43 |
44 |
45 |
46 |
setGrid(true)}
50 | />
51 |
52 | setGrid(false)} />
53 |
54 |
55 | {filteredProducts.length} Products found.
56 |
57 |
58 | {/* Search Icon */}
59 |
60 | setSearch(e.target.value)} />
61 |
62 | {/* Sort Products */}
63 |
64 |
65 |
72 |
73 |
74 |
75 |
76 | {products.lenght === 0 ? (
77 |
No product found.
78 | ) : (
79 | <>
80 | {currentProducts.map((product) => {
81 | return (
82 |
85 | );
86 | })}
87 | >
88 | )}
89 |
95 |
96 |
97 | );
98 | };
99 |
100 | export default ProductList;
101 |
--------------------------------------------------------------------------------
/src/components/product/productList/ProductList.module.scss:
--------------------------------------------------------------------------------
1 | .product-list {
2 | width: 100%;
3 | .top {
4 | width: 100%;
5 | border-bottom: 2px solid #ccc;
6 | display: flex;
7 | justify-content: space-between;
8 | align-items: center;
9 | // flex-wrap: wrap;
10 | .icons {
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | & > * {
15 | margin-right: 7px;
16 | cursor: pointer;
17 | }
18 | }
19 |
20 | .sort {
21 | label {
22 | font-size: 1.4rem;
23 | font-weight: 500;
24 | margin: 0 5px;
25 | }
26 | select {
27 | font-size: 1.6rem;
28 | font-weight: 300;
29 | border: none;
30 | // border-bottom: 1px solid #777;
31 | outline: none;
32 | }
33 | }
34 | }
35 |
36 | // .search {
37 | // margin: 5px 0;
38 | // position: relative;
39 | // flex: 1;
40 |
41 | // .icon {
42 | // position: absolute;
43 | // top: 50%;
44 | // left: 1rem;
45 | // transform: translateY(-50%);
46 | // }
47 |
48 | // input[type="text"] {
49 | // display: block;
50 | // font-size: 1.6rem;
51 | // font-weight: 300;
52 | // padding: 1rem;
53 | // padding-left: 3rem;
54 | // margin: 1rem auto;
55 | // width: 100%;
56 | // border: 1px solid #777;
57 | // border-radius: 3px;
58 | // outline: none;
59 | // }
60 | // }
61 | @media screen and (max-width: 800px) {
62 | .top {
63 | flex-direction: column;
64 | align-items: start;
65 | padding-bottom: 5px;
66 | }
67 | .search {
68 | width: 100%;
69 | }
70 | }
71 | }
72 |
73 | .grid {
74 | display: flex;
75 | justify-content: space-around;
76 |
77 | flex-wrap: wrap;
78 | // padding: 1rem 0;
79 | background-color: #fffdf7;
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/reviewProducts/ReviewProducts.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useSelector } from "react-redux";
3 | import { useParams } from "react-router-dom";
4 | import { selectUserID, selectUserName } from "../../redux/slice/authSlice";
5 | import Card from "../card/Card";
6 | import styles from "./ReviewProducts.module.scss";
7 | import StarsRating from "react-star-rate";
8 | import { addDoc, collection, Timestamp } from "firebase/firestore";
9 | import { db } from "../../firebase/config";
10 | import { toast } from "react-toastify";
11 | import useFetchDocument from "../../customHooks/useFetchDocument";
12 | import spinnerImg from "../../assets/spinner.jpg";
13 |
14 | const ReviewProducts = () => {
15 | const [rate, setRate] = useState(0);
16 | const [review, setReview] = useState("");
17 | const [product, setProduct] = useState(null);
18 | const { id } = useParams();
19 | const { document } = useFetchDocument("products", id);
20 | const userID = useSelector(selectUserID);
21 | const userName = useSelector(selectUserName);
22 |
23 | useEffect(() => {
24 | setProduct(document);
25 | }, [document]);
26 |
27 | const submitReview = (e) => {
28 | e.preventDefault();
29 |
30 | const today = new Date();
31 | const date = today.toDateString();
32 | const reviewConfig = {
33 | userID,
34 | userName,
35 | productID: id,
36 | rate,
37 | review,
38 | reviewDate: date,
39 | createdAt: Timestamp.now().toDate(),
40 | };
41 | try {
42 | addDoc(collection(db, "reviews"), reviewConfig);
43 | toast.success("Review submitted successfully");
44 | setRate(0);
45 | setReview("");
46 | } catch (error) {
47 | toast.error(error.message);
48 | }
49 | };
50 |
51 | return (
52 |
53 |
54 |
Review Products
55 | {product === null ? (
56 |

57 | ) : (
58 | <>
59 |
60 | Product name: {product.name}
61 |
62 |

67 | >
68 | )}
69 |
70 |
71 |
91 |
92 |
93 |
94 | );
95 | };
96 |
97 | export default ReviewProducts;
98 |
--------------------------------------------------------------------------------
/src/components/reviewProducts/ReviewProducts.module.scss:
--------------------------------------------------------------------------------
1 | .review {
2 | .card {
3 | width: 100%;
4 | max-width: 500px;
5 | padding: 1rem;
6 | }
7 | form {
8 | label {
9 | display: block;
10 | font-size: 1.4rem;
11 | font-weight: 500;
12 | }
13 | input[type="text"],
14 | input[type="number"],
15 | input[type="file"],
16 | input[type="email"],
17 | select,
18 | textarea,
19 | input[type="password"] {
20 | display: block;
21 | font-size: 1.6rem;
22 | font-weight: 300;
23 | padding: 1rem;
24 | margin: 1rem auto;
25 | width: 100%;
26 | border: 1px solid #777;
27 | border-radius: 3px;
28 | outline: none;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/search/Search.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Search.module.scss";
3 | import { BiSearch } from "react-icons/bi";
4 |
5 | const Search = ({ value, onChange }) => {
6 | return (
7 |
8 |
9 |
10 |
16 |
17 | );
18 | };
19 |
20 | export default Search;
21 |
--------------------------------------------------------------------------------
/src/components/search/Search.module.scss:
--------------------------------------------------------------------------------
1 | .search {
2 | margin: 5px 0;
3 | position: relative;
4 | flex: 1;
5 |
6 | .icon {
7 | position: absolute;
8 | top: 50%;
9 | left: 1rem;
10 | transform: translateY(-50%);
11 | }
12 |
13 | input[type="text"] {
14 | display: block;
15 | font-size: 1.6rem;
16 | font-weight: 300;
17 | padding: 1rem;
18 | padding-left: 3rem;
19 | margin: 1rem auto;
20 | width: 100%;
21 | border: 1px solid #777;
22 | border-radius: 3px;
23 | outline: none;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/slider/Slider.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { AiOutlineArrowLeft, AiOutlineArrowRight } from "react-icons/ai";
3 | import { sliderData } from "./slider-data";
4 | import "./Slider.scss";
5 |
6 | const Slider = () => {
7 | const [currentSlide, setCurrentSlide] = useState(0);
8 | const slideLength = sliderData.length;
9 | // console.log(slideLength);
10 |
11 | const autoScroll = true;
12 | let slideInterval;
13 | let intervalTime = 5000;
14 |
15 | const nextSlide = () => {
16 | setCurrentSlide(currentSlide === slideLength - 1 ? 0 : currentSlide + 1);
17 | };
18 |
19 | const prevSlide = () => {
20 | setCurrentSlide(currentSlide === 0 ? slideLength - 1 : currentSlide - 1);
21 | };
22 |
23 | useEffect(() => {
24 | setCurrentSlide(0);
25 | }, []);
26 |
27 | // const auto = () => {
28 | // slideInterval = setInterval(nextSlide, intervalTime);
29 | // };
30 |
31 | useEffect(() => {
32 | if (autoScroll) {
33 | const auto = () => {
34 | slideInterval = setInterval(nextSlide, intervalTime);
35 | };
36 | auto();
37 | }
38 | return () => clearInterval(slideInterval);
39 | }, [currentSlide, slideInterval, autoScroll]);
40 |
41 | return (
42 |
43 |
44 |
45 |
46 | {sliderData.map((slide, index) => {
47 | const { image, heading, desc } = slide;
48 | return (
49 |
53 | {index === currentSlide && (
54 | <>
55 |

56 |
64 | >
65 | )}
66 |
67 | );
68 | })}
69 |
70 | );
71 | };
72 |
73 | export default Slider;
74 |
--------------------------------------------------------------------------------
/src/components/slider/Slider.scss:
--------------------------------------------------------------------------------
1 | .slider {
2 | width: 100%;
3 | height: 90vh;
4 | position: relative;
5 | overflow: hidden;
6 | background-color: var(--color-dark);
7 | }
8 |
9 | .slide {
10 | position: absolute;
11 | top: 0;
12 | left: 0;
13 | width: 100%;
14 | height: 100%;
15 | opacity: 0;
16 | transform: translateX(-50%);
17 | transition: all 0.5s ease;
18 | }
19 |
20 | @media screen and (min-width: 600px) {
21 | .slide img {
22 | width: 100%;
23 | height: 100%;
24 | }
25 | }
26 |
27 | .slide img {
28 | height: 100%;
29 | }
30 |
31 | .content {
32 | position: absolute;
33 | text-align: center;
34 | top: 23rem;
35 | left: 50%;
36 | opacity: 0;
37 | width: 50%;
38 | padding: 3rem;
39 | display: flex;
40 | justify-self: center;
41 | align-items: center;
42 | flex-direction: column;
43 | transform: translateX(-50%);
44 | background: rgba(0, 0, 0, 0.4);
45 | animation: slide-up 1s ease 0.5s;
46 | animation-fill-mode: forwards;
47 | // visibility: hidden;
48 | h2 {
49 | font-size: 4.5rem;
50 | }
51 | }
52 |
53 | @keyframes slide-up {
54 | 0% {
55 | visibility: visible;
56 | top: 23rem;
57 | }
58 | 100% {
59 | visibility: visible;
60 | top: 17rem;
61 | }
62 | }
63 |
64 | @media screen and (max-width: 600px) {
65 | .content {
66 | width: 80%;
67 | }
68 | }
69 |
70 | .content > * {
71 | color: #fff;
72 | margin-bottom: 1rem;
73 | }
74 |
75 | .current {
76 | opacity: 1;
77 | transform: translateX(0);
78 | }
79 |
80 | .current .content {
81 | opacity: 1;
82 | }
83 | .arrow {
84 | border: 2px solid orangered;
85 | border-radius: 50%;
86 | background: transparent;
87 | color: #fff;
88 | width: 2.5rem;
89 | height: 2.5rem;
90 | cursor: pointer;
91 | position: absolute;
92 | top: 50%;
93 | transform: translateY(-50%);
94 | z-index: 2;
95 | }
96 |
97 | .arrow:hover {
98 | background: #fff;
99 | }
100 |
101 | .next {
102 | right: 1.5rem;
103 | color: orangered;
104 | }
105 | .prev {
106 | left: 1.5rem;
107 | color: orangered;
108 | }
109 |
110 | hr {
111 | height: 2px;
112 | background: #fff;
113 | width: 50%;
114 | }
115 |
--------------------------------------------------------------------------------
/src/components/slider/slider-data.js:
--------------------------------------------------------------------------------
1 | export const sliderData = [
2 | {
3 | image: "https://i.ibb.co/CBGRLhG/bg-4.jpg",
4 | heading: "Shoes Villa",
5 | desc: "Up to 30% off on all onsale proucts.",
6 | },
7 | {
8 | image: "https://i.ibb.co/cDLBk5h/bg-1.jpg",
9 | heading: "Women Fashion",
10 | desc: "Up to 30% off on all onsale proucts.",
11 | },
12 | {
13 | image: "https://i.ibb.co/HXjD3V0/bg-2.jpg",
14 | heading: "Men Fashion",
15 | desc: "Up to 30% off on all onsale proucts.",
16 | },
17 | {
18 | image: "https://i.ibb.co/H2FRmtV/bg-3.jpg",
19 | heading: "Awesome Gadgets",
20 | desc: "Up to 30% off on all onsale proucts.",
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/src/components/text:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/src/components/text
--------------------------------------------------------------------------------
/src/customHooks/useFetchCollection.js:
--------------------------------------------------------------------------------
1 | import { collection, onSnapshot, orderBy, query } from "firebase/firestore";
2 | import { useEffect, useState } from "react";
3 | import { toast } from "react-toastify";
4 | import { db } from "../firebase/config";
5 |
6 | const useFetchCollection = (collectionName) => {
7 | const [data, setData] = useState([]);
8 | const [isLoading, setIsLoading] = useState(false);
9 |
10 | const getCollection = () => {
11 | setIsLoading(true);
12 | try {
13 | const docRef = collection(db, collectionName);
14 | const q = query(docRef, orderBy("createdAt", "desc"));
15 | onSnapshot(q, (snapshot) => {
16 | // console.log(snapshot.docs);
17 | const allData = snapshot.docs.map((doc) => ({
18 | id: doc.id,
19 | ...doc.data(),
20 | }));
21 | // console.log(allData);
22 | setData(allData);
23 | setIsLoading(false);
24 | });
25 | } catch (error) {
26 | setIsLoading(false);
27 | toast.error(error.message);
28 | }
29 | };
30 |
31 | useEffect(() => {
32 | getCollection();
33 | }, []);
34 |
35 | return { data, isLoading };
36 | };
37 |
38 | export default useFetchCollection;
39 |
--------------------------------------------------------------------------------
/src/customHooks/useFetchDocument.js:
--------------------------------------------------------------------------------
1 | import { doc, getDoc } from "firebase/firestore";
2 | import { useEffect, useState } from "react";
3 | import { toast } from "react-toastify";
4 | import { db } from "../firebase/config";
5 |
6 | const useFetchDocument = (collectionName, documentID) => {
7 | const [document, setDocument] = useState(null);
8 |
9 | const getDocument = async () => {
10 | const docRef = doc(db, collectionName, documentID);
11 | const docSnap = await getDoc(docRef);
12 |
13 | if (docSnap.exists()) {
14 | // console.log("Document data:", docSnap.data());
15 | const obj = {
16 | id: documentID,
17 | ...docSnap.data(),
18 | };
19 | setDocument(obj);
20 | } else {
21 | toast.error("Document not found");
22 | }
23 | };
24 |
25 | useEffect(() => {
26 | getDocument();
27 | }, []);
28 |
29 | return { document };
30 | };
31 |
32 | export default useFetchDocument;
33 |
--------------------------------------------------------------------------------
/src/firebase/config.js:
--------------------------------------------------------------------------------
1 | import { initializeApp } from "firebase/app";
2 | import { getAuth } from "firebase/auth";
3 | import { getFirestore } from "firebase/firestore";
4 | import { getStorage } from "firebase/storage";
5 |
6 | // Your web app's Firebase configuration
7 | export const firebaseConfig = {
8 | apiKey: process.env.REACT_APP_FB_API_KEY,
9 | authDomain: "eshop-ea8e7.firebaseapp.com",
10 | projectId: "eshop-ea8e7",
11 | storageBucket: "eshop-ea8e7.appspot.com",
12 | messagingSenderId: "146569443233",
13 | appId: "1:146569443233:web:46db000fc2c897f3b73de0",
14 | };
15 |
16 | // Initialize Firebase
17 | const app = initializeApp(firebaseConfig);
18 | export const auth = getAuth(app);
19 | export const db = getFirestore(app);
20 | export const storage = getStorage(app);
21 |
22 | export default app;
23 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap");
2 |
3 | :root {
4 | --font-family: "Poppins", sans-serif;
5 | --dark-blue: #0a1930;
6 | --light-blue: #1f93ff;
7 |
8 | --color-white: #fff;
9 | --color-dark: #333;
10 |
11 | --color-grey: #eee;
12 | --box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
13 |
14 | --color-purple: #9d0191;
15 | --color-orange: #ff7722;
16 |
17 | --color-primary: #007bff;
18 | --color-success: #28a745;
19 | --color-danger: orangered;
20 | }
21 |
22 | * {
23 | margin: 0;
24 | padding: 0;
25 | box-sizing: border-box;
26 | scroll-behavior: smooth;
27 | }
28 |
29 | html {
30 | font-size: 10px;
31 | }
32 |
33 | body {
34 | font-family: var(--font-family);
35 | }
36 |
37 | section {
38 | width: 100%;
39 | padding: 4rem 0;
40 | }
41 |
42 | .container {
43 | max-width: 1000px;
44 | margin: 0 auto;
45 | padding: 0 20px;
46 | }
47 |
48 | /* UTILITY CLASSES */
49 |
50 | /* Flex */
51 | .--flex-center {
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | }
56 | .--flex-start {
57 | display: flex;
58 | justify-content: flex-start;
59 | align-items: flex-start;
60 | }
61 | .--flex-end {
62 | display: flex;
63 | justify-content: flex-end;
64 | align-items: center;
65 | }
66 | .--flex-between {
67 | display: flex;
68 | justify-content: space-between;
69 | align-items: center;
70 | }
71 |
72 | .--dir-column {
73 | flex-direction: column;
74 | }
75 |
76 | .--flex-dir-column {
77 | display: flex;
78 | }
79 |
80 | .--align-center {
81 | display: flex;
82 | align-items: center;
83 | }
84 |
85 | .--100vh {
86 | height: 100vh;
87 | }
88 |
89 | .--mh-100vh {
90 | min-height: 100vh;
91 | }
92 |
93 | /* Grid */
94 | .--grid-15 {
95 | display: grid;
96 | grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
97 | row-gap: 1rem;
98 | column-gap: 1rem;
99 | }
100 | .--grid-25 {
101 | display: grid;
102 | grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
103 | row-gap: 1rem;
104 | column-gap: 1rem;
105 | }
106 |
107 | /* Center All */
108 | .--center-all {
109 | display: flex;
110 | justify-content: center;
111 | align-items: center;
112 | flex-direction: column;
113 | width: 100%;
114 | margin: auto;
115 | text-align: center;
116 | }
117 |
118 | /* Heading */
119 | h1,
120 | h2,
121 | h3,
122 | h4 {
123 | font-weight: 500;
124 | line-height: 1.2;
125 | color: var(--color-dark);
126 | margin-bottom: 1rem;
127 | }
128 | h1 {
129 | font-size: 4rem;
130 | }
131 | h2 {
132 | font-size: 3rem;
133 | }
134 | h3 {
135 | font-size: 2.5rem;
136 | }
137 | h4 {
138 | font-size: 2rem;
139 | }
140 |
141 | p {
142 | font-size: 1.5rem;
143 | font-weight: 300;
144 | line-height: 1.3;
145 | color: var(--color-dark);
146 | }
147 | .--text-xl {
148 | font-size: 4.5rem;
149 | }
150 | .--text-lg {
151 | font-size: 4rem;
152 | }
153 |
154 | .--text-md {
155 | font-size: 3rem;
156 | }
157 |
158 | .--text-sm {
159 | font-size: 1.2rem;
160 | font-weight: 300;
161 | }
162 |
163 | .--text-p {
164 | font-size: 1.5rem;
165 | font-weight: 300;
166 | line-height: 1.3;
167 | color: var(--color-dark);
168 | }
169 |
170 | .--fw-bold {
171 | font-weight: 600;
172 | }
173 | .--fw-thin {
174 | font-weight: 200;
175 | }
176 |
177 | /* Text Color */
178 | .--text-light {
179 | color: #fff;
180 | }
181 |
182 | .--color-primary {
183 | color: #007bff;
184 | }
185 | .--color-danger {
186 | color: orangered;
187 | }
188 | .--color-success {
189 | color: #28a745;
190 | }
191 |
192 | .--color-white {
193 | color: #fff;
194 | }
195 |
196 | /* Center Text */
197 | .--text-center {
198 | text-align: center;
199 | }
200 |
201 | /* Card */
202 | .--card {
203 | border: 1px solid transparent;
204 | border-radius: 5px;
205 | box-shadow: var(--box-shadow);
206 | overflow: hidden;
207 | }
208 |
209 | /* Margin */
210 | .--m {
211 | margin: 1rem;
212 | }
213 | .--ml {
214 | margin-left: 1rem;
215 | }
216 | .--mr {
217 | margin-right: 1rem;
218 | }
219 |
220 | .--mb {
221 | margin-bottom: 1rem;
222 | }
223 |
224 | .--my {
225 | margin: 1rem 0;
226 | }
227 | .--mx {
228 | margin: 0 1rem;
229 | }
230 |
231 | .--m2 {
232 | margin: 2rem;
233 | }
234 |
235 | .--ml2 {
236 | margin-left: 2rem;
237 | }
238 | .--mr2 {
239 | margin-right: 2rem;
240 | }
241 |
242 | .--mb2 {
243 | margin-bottom: 2rem;
244 | }
245 |
246 | .--my2 {
247 | margin: 2rem 0;
248 | }
249 |
250 | .--mx2 {
251 | margin: 0 2rem;
252 | }
253 |
254 | /* Padding */
255 | .--p {
256 | padding: 1rem;
257 | }
258 | .--p2 {
259 | padding: 2rem;
260 | }
261 | .--py {
262 | padding: 1rem 0;
263 | }
264 | .--py2 {
265 | padding: 2rem 0;
266 | }
267 | .--px {
268 | padding: 0 1rem;
269 | }
270 | .--px2 {
271 | padding: 0 2rem;
272 | }
273 |
274 | .--btn {
275 | font-size: 1.6rem;
276 | font-weight: 400;
277 | padding: 6px 8px;
278 | margin: 0 5px 0 0;
279 | border: 1px solid transparent;
280 | border-radius: 3px;
281 | cursor: pointer;
282 | display: flex;
283 | justify-content: center;
284 | align-items: center;
285 | transition: all 0.3s;
286 | }
287 |
288 | .--btn:hover {
289 | transform: translateY(-2px);
290 | }
291 |
292 | .--btn-lg {
293 | padding: 8px 10px;
294 | }
295 |
296 | .--btn-block {
297 | width: 100%;
298 | }
299 |
300 | .--btn-primary {
301 | color: #fff;
302 | background: #007bff;
303 | }
304 | .--btn-secondary {
305 | color: #fff;
306 | border: 1px solid #fff;
307 | background: transparent;
308 | }
309 | .--btn-danger {
310 | color: #fff;
311 | background: orangered;
312 | }
313 |
314 | .--btn-success {
315 | color: #fff;
316 | background: #28a745;
317 | }
318 |
319 | /* Background */
320 | .--bg-light {
321 | background: #fff;
322 | }
323 | .--bg-dark {
324 | background: var(--color-dark);
325 | }
326 | .--bg-primary {
327 | background: var(--color-primary);
328 | }
329 | .--bg-success {
330 | background: var(--color-success);
331 | }
332 | .--bg-grey {
333 | background: var(--color-grey);
334 | }
335 |
336 | .--form-control {
337 | font-size: 1.6rem;
338 | font-weight: 300;
339 | }
340 |
341 | .--form-control > * {
342 | margin: 5px 0;
343 | }
344 |
345 | .--form-control input {
346 | font-size: 1.6rem;
347 | font-weight: 300;
348 | padding: 8px 1rem;
349 | border: 1px solid #777;
350 | border-radius: 3px;
351 | outline: none;
352 | }
353 | .--form-control select {
354 | font-size: 1.4rem;
355 | font-weight: 400;
356 | padding: 8px 1rem;
357 | border: 1px solid #777;
358 | border-radius: 3px;
359 | }
360 |
361 | .--form-control label {
362 | font-size: 1.6rem;
363 | font-weight: 400;
364 | display: inline-block;
365 | min-width: 7rem;
366 | color: var(--color-dark);
367 | margin-right: 1rem;
368 | }
369 |
370 | @media screen and (max-width: 600px) {
371 | .--flex-dir-column {
372 | flex-direction: column;
373 | }
374 | }
375 |
376 | .--block {
377 | display: block;
378 | }
379 | .--inline-block {
380 | display: inline-block;
381 | }
382 |
383 | .--width-100 {
384 | width: 100%;
385 | }
386 |
387 | .--width-500px {
388 | width: 500px;
389 | }
390 |
391 | .--line {
392 | position: relative;
393 | }
394 | .--line::after {
395 | content: "";
396 | position: absolute;
397 | left: 50%;
398 | bottom: -5px;
399 | transform: translateX(-50%);
400 | width: 5rem;
401 | height: 3px;
402 | margin-bottom: 1rem;
403 |
404 | background: rgb(217, 8, 170);
405 | background: linear-gradient(
406 | 135deg,
407 | rgba(163, 1, 191, 1) 44%,
408 | rgba(217, 8, 170, 1) 57%
409 | );
410 | }
411 |
412 | .--list-style-none {
413 | list-style: none;
414 | }
415 |
416 | .--profile-img {
417 | width: 6rem;
418 | height: 6rem;
419 | border: 1px solid #ccc;
420 | border-radius: 50%;
421 | }
422 |
423 | a {
424 | font-size: 1.4rem;
425 | color: var(--dark-blue);
426 | text-decoration: none;
427 | transition: all 0.2s;
428 | }
429 |
430 | a:hover {
431 | color: var(--color-dark);
432 | font-size: 1.5rem;
433 | }
434 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 | import { Provider } from "react-redux";
6 | import store from "./redux/store";
7 |
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.getElementById("root")
13 | );
14 |
--------------------------------------------------------------------------------
/src/pages/admin/Admin.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Routes } from "react-router-dom";
3 | import AddProduct from "../../components/admin/addProduct/AddProduct";
4 | import Home from "../../components/admin/home/Home";
5 | import Navbar from "../../components/admin/navbar/Navbar";
6 | import OrderDetails from "../../components/admin/orderDetails/OrderDetails";
7 | import Orders from "../../components/admin/orders/Orders";
8 | import ViewProducts from "../../components/admin/viewProducts/ViewProducts";
9 |
10 | import styles from "./Admin.module.scss";
11 |
12 | const Admin = () => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 | } />
21 | } />
22 | } />
23 | } />
24 | } />
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default Admin;
32 |
--------------------------------------------------------------------------------
/src/pages/admin/Admin.module.scss:
--------------------------------------------------------------------------------
1 | .admin {
2 | display: flex;
3 |
4 | .navbar {
5 | width: 25%;
6 | min-height: 80vh;
7 | }
8 | .content {
9 | width: 75%;
10 | padding: 1rem;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/pages/auth/Login.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import styles from "./auth.module.scss";
3 | import loginImg from "../../assets/login.png";
4 | import { Link, useNavigate } from "react-router-dom";
5 | import { FaGoogle } from "react-icons/fa";
6 | import Card from "../../components/card/Card";
7 | import {
8 | GoogleAuthProvider,
9 | signInWithEmailAndPassword,
10 | signInWithPopup,
11 | } from "firebase/auth";
12 | import { auth } from "../../firebase/config";
13 | import { toast } from "react-toastify";
14 | import Loader from "../../components/loader/Loader";
15 | import { useSelector } from "react-redux";
16 | import { selectPreviousURL } from "../../redux/slice/cartSlice";
17 |
18 | const Login = () => {
19 | const [email, setEmail] = useState("");
20 | const [password, setPassword] = useState("");
21 | const [isLoading, setIsLoading] = useState(false);
22 |
23 | const previousURL = useSelector(selectPreviousURL);
24 | const navigate = useNavigate();
25 |
26 | const redirectUser = () => {
27 | if (previousURL.includes("cart")) {
28 | return navigate("/cart");
29 | }
30 | navigate("/");
31 | };
32 |
33 | const loginUser = (e) => {
34 | e.preventDefault();
35 | setIsLoading(true);
36 |
37 | signInWithEmailAndPassword(auth, email, password)
38 | .then((userCredential) => {
39 | // const user = userCredential.user;
40 | setIsLoading(false);
41 | toast.success("Login Successful...");
42 | redirectUser();
43 | })
44 | .catch((error) => {
45 | setIsLoading(false);
46 | toast.error(error.message);
47 | });
48 | };
49 |
50 | // Login with Goooglr
51 | const provider = new GoogleAuthProvider();
52 | const signInWithGoogle = () => {
53 | signInWithPopup(auth, provider)
54 | .then((result) => {
55 | // const user = result.user;
56 | toast.success("Login Successfully");
57 | redirectUser();
58 | })
59 | .catch((error) => {
60 | toast.error(error.message);
61 | });
62 | };
63 |
64 | return (
65 | <>
66 | {isLoading && }
67 |
68 |
69 |

70 |
71 |
72 |
73 |
74 |
Login
75 |
76 |
99 |
105 |
106 | Don't have an account?
107 | Register
108 |
109 |
110 |
111 |
112 | >
113 | );
114 | };
115 |
116 | export default Login;
117 |
--------------------------------------------------------------------------------
/src/pages/auth/Register.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import styles from "./auth.module.scss";
3 | import registerImg from "../../assets/register.png";
4 | import Card from "../../components/card/Card";
5 | import { Link, useNavigate } from "react-router-dom";
6 | import { createUserWithEmailAndPassword } from "firebase/auth";
7 | import { auth } from "../../firebase/config";
8 | import Loader from "../../components/loader/Loader";
9 | import { toast } from "react-toastify";
10 |
11 | const Register = () => {
12 | const [email, setEmail] = useState("");
13 | const [password, setPassword] = useState("");
14 | const [cPassword, setCPassword] = useState("");
15 | const [isLoading, setIsLoading] = useState(false);
16 |
17 | const navigate = useNavigate();
18 |
19 | const registerUser = (e) => {
20 | e.preventDefault();
21 | if (password !== cPassword) {
22 | toast.error("Passwords do not match.");
23 | }
24 | setIsLoading(true);
25 |
26 | createUserWithEmailAndPassword(auth, email, password)
27 | .then((userCredential) => {
28 | const user = userCredential.user;
29 | console.log(user);
30 | setIsLoading(false);
31 | toast.success("Registration Successful...");
32 | navigate("/login");
33 | })
34 | .catch((error) => {
35 | toast.error(error.message);
36 | setIsLoading(false);
37 | });
38 | };
39 |
40 | return (
41 | <>
42 | {isLoading && }
43 |
44 |
45 |
80 |
81 |
82 |

83 |
84 |
85 | >
86 | );
87 | };
88 |
89 | export default Register;
90 |
--------------------------------------------------------------------------------
/src/pages/auth/Reset.js:
--------------------------------------------------------------------------------
1 | import styles from "./auth.module.scss";
2 | import { Link } from "react-router-dom";
3 | import resetImg from "../../assets/forgot.png";
4 | import Card from "../../components/card/Card";
5 | import { useState } from "react";
6 | import { toast } from "react-toastify";
7 | import { auth } from "../../firebase/config";
8 | import { sendPasswordResetEmail } from "firebase/auth";
9 | import Loader from "../../components/loader/Loader";
10 |
11 | const Reset = () => {
12 | const [email, setEmail] = useState("");
13 | const [isLoading, setIsLoading] = useState(false);
14 |
15 | const resetPassword = (e) => {
16 | e.preventDefault();
17 | setIsLoading(true);
18 |
19 | sendPasswordResetEmail(auth, email)
20 | .then(() => {
21 | setIsLoading(false);
22 | toast.success("Check your email for a reset link");
23 | })
24 | .catch((error) => {
25 | setIsLoading(false);
26 | toast.error(error.message);
27 | });
28 | };
29 |
30 | return (
31 | <>
32 | {isLoading && }
33 |
34 |
35 |

36 |
37 |
38 |
39 |
40 |
Reset Password
41 |
42 |
63 |
64 |
65 |
66 | >
67 | );
68 | };
69 |
70 | export default Reset;
71 |
--------------------------------------------------------------------------------
/src/pages/auth/auth.module.scss:
--------------------------------------------------------------------------------
1 | .auth {
2 | min-height: 80vh;
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 |
7 | .img {
8 | animation: slide-down 0.5s ease;
9 | }
10 |
11 | .form {
12 | width: 35rem;
13 | padding: 1.5rem;
14 | animation: slide-up 0.5s ease;
15 | h2 {
16 | color: var(--color-danger);
17 | text-align: center;
18 | }
19 | form {
20 | input[type="text"],
21 | input[type="email"],
22 | input[type="password"] {
23 | display: block;
24 | font-size: 1.6rem;
25 | font-weight: 300;
26 | padding: 1rem;
27 | margin: 1rem auto;
28 | width: 100%;
29 | border: 1px solid #777;
30 | border-radius: 3px;
31 | outline: none;
32 | }
33 | .links {
34 | display: flex;
35 | justify-content: space-between;
36 | margin: 5px 0;
37 | }
38 |
39 | p {
40 | text-align: center;
41 | margin: 1rem;
42 | }
43 | }
44 | .register {
45 | display: flex;
46 | justify-content: center;
47 | align-items: center;
48 | margin-top: 1rem;
49 | }
50 | }
51 |
52 | @keyframes slide-up {
53 | 0% {
54 | transform: translateY(-5rem);
55 | }
56 | 100% {
57 | transform: translateY(0);
58 | }
59 | }
60 | @keyframes slide-down {
61 | 0% {
62 | transform: translateY(5rem);
63 | }
64 | 100% {
65 | transform: translateY(0);
66 | }
67 | }
68 | }
69 |
70 | @media screen and (max-width: 700px) {
71 | .img {
72 | display: none;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/pages/cart/Cart.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import {
4 | ADD_TO_CART,
5 | CALCULATE_SUBTOTAL,
6 | CALCULATE_TOTAL_QUANTITY,
7 | CLEAR_CART,
8 | DECREASE_CART,
9 | REMOVE_FROM_CART,
10 | SAVE_URL,
11 | selectCartItems,
12 | selectCartTotalAmount,
13 | selectCartTotalQuantity,
14 | } from "../../redux/slice/cartSlice";
15 | import styles from "./Cart.module.scss";
16 | import { FaTrashAlt } from "react-icons/fa";
17 | import { Link, useNavigate } from "react-router-dom";
18 | import Card from "../../components/card/Card";
19 | import { selectIsLoggedIn } from "../../redux/slice/authSlice";
20 |
21 | const Cart = () => {
22 | const cartItems = useSelector(selectCartItems);
23 | const cartTotalAmount = useSelector(selectCartTotalAmount);
24 | const cartTotalQuantity = useSelector(selectCartTotalQuantity);
25 | const dispatch = useDispatch();
26 | const isLoggedIn = useSelector(selectIsLoggedIn);
27 |
28 | const navigate = useNavigate();
29 |
30 | const increaseCart = (cart) => {
31 | dispatch(ADD_TO_CART(cart));
32 | };
33 |
34 | const decreaseCart = (cart) => {
35 | dispatch(DECREASE_CART(cart));
36 | };
37 |
38 | const removeFromCart = (cart) => {
39 | dispatch(REMOVE_FROM_CART(cart));
40 | };
41 |
42 | const clearCart = () => {
43 | dispatch(CLEAR_CART());
44 | };
45 |
46 | useEffect(() => {
47 | dispatch(CALCULATE_SUBTOTAL());
48 | dispatch(CALCULATE_TOTAL_QUANTITY());
49 | dispatch(SAVE_URL(""));
50 | }, [cartItems, dispatch]);
51 |
52 | const url = window.location.href;
53 |
54 | const checkout = () => {
55 | if (isLoggedIn) {
56 | navigate("/checkout-details");
57 | } else {
58 | dispatch(SAVE_URL(url));
59 | navigate("/login");
60 | }
61 | };
62 |
63 | return (
64 |
65 |
66 |
Shopping Cart
67 | {cartItems.length === 0 ? (
68 | <>
69 |
Your cart is currently empty.
70 |
71 |
72 | ← Continue shopping
73 |
74 | >
75 | ) : (
76 | <>
77 |
78 |
79 |
80 | s/n |
81 | Product |
82 | Price |
83 | Quantity |
84 | Total |
85 | Action |
86 |
87 |
88 |
89 | {cartItems.map((cart, index) => {
90 | const { id, name, price, imageURL, cartQuantity } = cart;
91 | return (
92 |
93 | {index + 1} |
94 |
95 |
96 | {name}
97 |
98 |
103 | |
104 | {price} |
105 |
106 |
107 |
113 |
114 | {cartQuantity}
115 |
116 |
122 |
123 | |
124 | {(price * cartQuantity).toFixed(2)} |
125 |
126 | removeFromCart(cart)}
130 | />
131 | |
132 |
133 | );
134 | })}
135 |
136 |
137 |
138 |
141 |
142 |
143 | ← Continue shopping
144 |
145 |
146 |
147 |
148 | {`Cart item(s): ${cartTotalQuantity}`}
149 |
150 |
151 |
Subtotal:
152 | {`$${cartTotalAmount.toFixed(2)}`}
153 |
154 | Tax an shipping calculated at checkout
155 |
161 |
162 |
163 |
164 | >
165 | )}
166 |
167 |
168 | );
169 | };
170 |
171 | export default Cart;
172 |
--------------------------------------------------------------------------------
/src/pages/cart/Cart.module.scss:
--------------------------------------------------------------------------------
1 | .table {
2 | padding: 5px;
3 | width: 100%;
4 | overflow-x: auto;
5 |
6 | table {
7 | border-collapse: collapse;
8 | width: 100%;
9 | font-size: 1.4rem;
10 |
11 | thead {
12 | border-top: 2px solid var(--light-blue);
13 | border-bottom: 2px solid var(--light-blue);
14 | }
15 |
16 | th {
17 | border: 1px solid #eee;
18 | }
19 |
20 | th,
21 | td {
22 | vertical-align: top;
23 | text-align: left;
24 | padding: 8px;
25 | &.icons {
26 | > * {
27 | margin-right: 5px;
28 | cursor: pointer;
29 | }
30 | }
31 | }
32 |
33 | tr {
34 | border-bottom: 1px solid #ccc;
35 | }
36 |
37 | tr:nth-child(even) {
38 | background-color: #eee;
39 | }
40 | }
41 | .summary {
42 | margin-top: 2rem;
43 | display: flex;
44 | justify-content: space-between;
45 | align-items: start;
46 |
47 | .card {
48 | padding: 1rem;
49 | .text {
50 | display: flex;
51 | justify-content: space-between;
52 | align-items: center;
53 | h3 {
54 | color: var(--color-danger);
55 | }
56 | }
57 | button {
58 | margin-top: 5px;
59 | }
60 | }
61 | }
62 | }
63 | .count {
64 | display: flex;
65 | align-items: center;
66 | button {
67 | border: 1px solid var(--darkblue);
68 | }
69 | & > * {
70 | margin-right: 1rem;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/pages/checkout/Checkout.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { loadStripe } from "@stripe/stripe-js";
3 | import { Elements } from "@stripe/react-stripe-js";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import {
6 | CALCULATE_SUBTOTAL,
7 | CALCULATE_TOTAL_QUANTITY,
8 | selectCartItems,
9 | selectCartTotalAmount,
10 | } from "../../redux/slice/cartSlice";
11 | import { selectEmail } from "../../redux/slice/authSlice";
12 | import {
13 | selectBillingAddress,
14 | selectShippingAddress,
15 | } from "../../redux/slice/checkoutSlice";
16 | import { toast } from "react-toastify";
17 | import CheckoutForm from "../../components/checkoutForm/CheckoutForm";
18 |
19 | const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PK);
20 |
21 | const Checkout = () => {
22 | const [message, setMessage] = useState("Initializing checkout...");
23 | const [clientSecret, setClientSecret] = useState("");
24 |
25 | const cartItems = useSelector(selectCartItems);
26 | const totalAmount = useSelector(selectCartTotalAmount);
27 | const customerEmail = useSelector(selectEmail);
28 |
29 | const shippingAddress = useSelector(selectShippingAddress);
30 | const billingAddress = useSelector(selectBillingAddress);
31 |
32 | const dispatch = useDispatch();
33 | useEffect(() => {
34 | dispatch(CALCULATE_SUBTOTAL());
35 | dispatch(CALCULATE_TOTAL_QUANTITY());
36 | }, [dispatch, cartItems]);
37 |
38 | const description = `eShop payment: email: ${customerEmail}, Amount: ${totalAmount}`;
39 |
40 | useEffect(() => {
41 | // http://localhost:4242/create-payment-intent
42 | // Create PaymentIntent as soon as the page loads
43 | fetch("https://eshop-react-firebase.herokuapp.com/create-payment-intent", {
44 | method: "POST",
45 | headers: { "Content-Type": "application/json" },
46 | body: JSON.stringify({
47 | items: cartItems,
48 | userEmail: customerEmail,
49 | shipping: shippingAddress,
50 | billing: billingAddress,
51 | description,
52 | }),
53 | })
54 | .then((res) => {
55 | if (res.ok) {
56 | return res.json();
57 | }
58 | return res.json().then((json) => Promise.reject(json));
59 | })
60 | .then((data) => {
61 | setClientSecret(data.clientSecret);
62 | })
63 | .catch((error) => {
64 | setMessage("Failed to initialize checkout");
65 | toast.error("Something went wrong!!!");
66 | });
67 | }, []);
68 |
69 | const appearance = {
70 | theme: "stripe",
71 | };
72 | const options = {
73 | clientSecret,
74 | appearance,
75 | };
76 |
77 | return (
78 | <>
79 |
80 | {!clientSecret &&
{message}
}
81 |
82 | {clientSecret && (
83 |
84 |
85 |
86 | )}
87 | >
88 | );
89 | };
90 |
91 | export default Checkout;
92 |
--------------------------------------------------------------------------------
/src/pages/checkout/CheckoutDetails.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { CountryDropdown } from "react-country-region-selector";
3 | import { useDispatch } from "react-redux";
4 | import { useNavigate } from "react-router-dom";
5 | import Card from "../../components/card/Card";
6 | import CheckoutSummary from "../../components/checkoutSummary/CheckoutSummary.js";
7 | import {
8 | SAVE_BILLING_ADDRESS,
9 | SAVE_SHIPPING_ADDRESS,
10 | } from "../../redux/slice/checkoutSlice";
11 | import styles from "./CheckoutDetails.module.scss";
12 |
13 | const initialAddressState = {
14 | name: "",
15 | line1: "",
16 | line2: "",
17 | city: "",
18 | state: "",
19 | postal_code: "",
20 | country: "",
21 | phone: "",
22 | };
23 |
24 | const CheckoutDetails = () => {
25 | const [shippingAddress, setShippingAddress] = useState({
26 | ...initialAddressState,
27 | });
28 | const [billingAddress, setBillingAddress] = useState({
29 | ...initialAddressState,
30 | });
31 |
32 | const dispatch = useDispatch();
33 | const navigate = useNavigate();
34 |
35 | const handleShipping = (e) => {
36 | const { name, value } = e.target;
37 | setShippingAddress({
38 | ...shippingAddress,
39 | [name]: value,
40 | });
41 | };
42 |
43 | const handleBilling = (e) => {
44 | const { name, value } = e.target;
45 | setBillingAddress({
46 | ...billingAddress,
47 | [name]: value,
48 | });
49 | };
50 |
51 | const handleSubmit = (e) => {
52 | e.preventDefault();
53 | dispatch(SAVE_SHIPPING_ADDRESS(shippingAddress));
54 | dispatch(SAVE_BILLING_ADDRESS(billingAddress));
55 | navigate("/checkout");
56 | };
57 |
58 | return (
59 |
235 | );
236 | };
237 |
238 | export default CheckoutDetails;
239 |
--------------------------------------------------------------------------------
/src/pages/checkout/CheckoutDetails.module.scss:
--------------------------------------------------------------------------------
1 | .checkout {
2 | width: 100%;
3 | position: relative;
4 |
5 | .card {
6 | width: 100%;
7 | max-width: 500px;
8 | padding: 1rem;
9 | h3 {
10 | font-weight: 300;
11 | }
12 | }
13 |
14 | form {
15 | width: 100%;
16 | display: flex;
17 |
18 | div {
19 | width: 100%;
20 | }
21 |
22 | label {
23 | display: block;
24 | font-size: 1.4rem;
25 | font-weight: 500;
26 | }
27 | input[type="text"],
28 | .select,
29 | .card-details {
30 | display: block;
31 | font-size: 1.6rem;
32 | font-weight: 300;
33 | padding: 1rem;
34 | margin: 1rem auto;
35 | width: 100%;
36 | border: 1px solid #777;
37 | border-radius: 3px;
38 | outline: none;
39 | }
40 | }
41 | }
42 |
43 | @media screen and (max-width: 700px) {
44 | .checkout {
45 | form {
46 | flex-direction: column;
47 | div {
48 | width: 100%;
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/pages/checkout/CheckoutSuccess.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | const CheckoutSuccess = () => {
4 | return (
5 |
6 |
7 |
Checkout Successful
8 |
Thank you for your purchase
9 |
10 |
11 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default CheckoutSuccess;
20 |
--------------------------------------------------------------------------------
/src/pages/contact/Contact.js:
--------------------------------------------------------------------------------
1 | import { useRef } from "react";
2 | import Card from "../../components/card/Card";
3 | import styles from "./Contact.module.scss";
4 | import { FaPhoneAlt, FaEnvelope, FaTwitter } from "react-icons/fa";
5 | import { GoLocation } from "react-icons/go";
6 | import emailjs from "@emailjs/browser";
7 | import { toast } from "react-toastify";
8 |
9 | const Contact = () => {
10 | const form = useRef();
11 |
12 | const sendEmail = (e) => {
13 | e.preventDefault();
14 | console.log(form.current);
15 |
16 | emailjs
17 | .sendForm(
18 | process.env.REACT_APP_EMAILJS_SERVICE_ID,
19 | "template_7xyhwen",
20 | form.current,
21 | "user_hKs2aRfLoozcqA28UpUyz"
22 | )
23 | .then(
24 | (result) => {
25 | toast.success("Message sent successfully");
26 | },
27 | (error) => {
28 | toast.error(error.text);
29 | }
30 | );
31 | e.target.reset();
32 | };
33 |
34 | return (
35 |
36 |
37 |
Contact Us
38 |
39 |
67 |
68 |
69 |
70 | Our Contact Information
71 | Fill the form or contact us via other channels listed below
72 |
73 |
74 |
75 | +234 705 141 6545
76 |
77 |
78 |
79 | Support@eshop.com
80 |
81 |
82 |
83 | Abuja, Nigeria
84 |
85 |
86 |
87 | @ZinoTrust
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | );
96 | };
97 |
98 | export default Contact;
99 |
--------------------------------------------------------------------------------
/src/pages/contact/Contact.module.scss:
--------------------------------------------------------------------------------
1 | .contact {
2 | .card {
3 | padding: 1rem;
4 | border: 1px solid #ccc;
5 | }
6 |
7 | .card2 {
8 | padding: 2rem;
9 | background-color: var(--light-blue);
10 | color: #fff;
11 | h3,
12 | p {
13 | color: #fff;
14 | }
15 | .icons {
16 | margin: 3rem 0;
17 | span {
18 | display: flex;
19 | justify-content: start;
20 | align-items: center;
21 | margin-bottom: 1rem;
22 | a,
23 | p {
24 | margin-left: 5px;
25 | }
26 | }
27 | }
28 | }
29 |
30 | .section {
31 | display: flex;
32 | align-items: start;
33 |
34 | form {
35 | width: 500px;
36 | max-width: 100%;
37 | margin-right: 1rem;
38 | margin-bottom: 1rem;
39 | label {
40 | display: block;
41 | font-size: 1.4rem;
42 | font-weight: 500;
43 | }
44 | input[type="text"],
45 | input[type="number"],
46 | input[type="file"],
47 | input[type="email"],
48 | select,
49 | textarea,
50 | input[type="password"] {
51 | display: block;
52 | font-size: 1.6rem;
53 | font-weight: 300;
54 | padding: 1rem;
55 | margin: 1rem auto;
56 | width: 100%;
57 | border: 1px solid #777;
58 | border-radius: 3px;
59 | outline: none;
60 | }
61 | }
62 | }
63 | }
64 |
65 | @media screen and (max-width: 700px) {
66 | .contact {
67 | .section {
68 | flex-direction: column;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/pages/home/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import Product from "../../components/product/Product";
3 | import Slider from "../../components/slider/Slider";
4 |
5 | const Home = () => {
6 | const url = window.location.href;
7 |
8 | useEffect(() => {
9 | const scrollToProducts = () => {
10 | if (url.includes("#products")) {
11 | window.scrollTo({
12 | top: 700,
13 | behavior: "smooth",
14 | });
15 | return;
16 | }
17 | };
18 | scrollToProducts();
19 | }, [url]);
20 |
21 | return (
22 |
26 | );
27 | };
28 |
29 | export default Home;
30 |
--------------------------------------------------------------------------------
/src/pages/home/Home.module.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/eshop-ecommerce/0eacd90433ca5a91e28e1bb5177aa71b5d7ed177/src/pages/home/Home.module.scss
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | export { default as Home } from "./home/Home";
2 | export { default as Contact } from "./contact/Contact";
3 | export { default as Login } from "./auth/Login";
4 | export { default as Register } from "./auth/Register";
5 | export { default as Reset } from "./auth/Reset";
6 | export { default as Admin } from "./admin/Admin";
7 |
--------------------------------------------------------------------------------
/src/pages/notFound/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import styles from "./NotFound.module.scss";
4 |
5 | const NotFound = () => {
6 | return (
7 |
8 |
9 |
404
10 |
Opppppsss, page not found.
11 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotFound;
20 |
--------------------------------------------------------------------------------
/src/pages/notFound/NotFound.module.scss:
--------------------------------------------------------------------------------
1 | .not-found {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | height: 80vh;
6 |
7 | div {
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | flex-direction: column;
12 | }
13 |
14 | h2 {
15 | font-size: 10rem;
16 | }
17 |
18 | p {
19 | margin-bottom: 2rem;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/pages/orderDetails/OrderDetails.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Link, useParams } from "react-router-dom";
3 | import useFetchDocument from "../../customHooks/useFetchDocument";
4 | import spinnerImg from "../../assets/spinner.jpg";
5 | import styles from "./OrderDetails.module.scss";
6 | const OrderDetails = () => {
7 | const [order, setOrder] = useState(null);
8 | const { id } = useParams();
9 | const { document } = useFetchDocument("orders", id);
10 |
11 | useEffect(() => {
12 | setOrder(document);
13 | }, [document]);
14 |
15 | return (
16 |
17 |
18 |
Order Details
19 |
20 | ← Back To Orders
21 |
22 |
23 | {order === null ? (
24 |

25 | ) : (
26 | <>
27 |
28 | Order ID {order.id}
29 |
30 |
31 | Order Amount ${order.orderAmount}
32 |
33 |
34 | Order Status {order.orderStatus}
35 |
36 |
37 |
38 |
39 |
40 | s/n |
41 | Product |
42 | Price |
43 | Quantity |
44 | Total |
45 | Action |
46 |
47 |
48 |
49 | {order.cartItems.map((cart, index) => {
50 | const { id, name, price, imageURL, cartQuantity } = cart;
51 | return (
52 |
53 |
54 | {index + 1}
55 | |
56 |
57 |
58 | {name}
59 |
60 |
65 | |
66 | {price} |
67 | {cartQuantity} |
68 | {(price * cartQuantity).toFixed(2)} |
69 |
70 |
71 |
74 |
75 | |
76 |
77 | );
78 | })}
79 |
80 |
81 | >
82 | )}
83 |
84 |
85 | );
86 | };
87 |
88 | export default OrderDetails;
89 |
--------------------------------------------------------------------------------
/src/pages/orderDetails/OrderDetails.module.scss:
--------------------------------------------------------------------------------
1 | .table {
2 | padding: 5px;
3 | width: 100%;
4 | overflow-x: auto;
5 |
6 | table {
7 | border-collapse: collapse;
8 | width: 100%;
9 | font-size: 1.4rem;
10 |
11 | thead {
12 | border-top: 2px solid var(--light-blue);
13 | border-bottom: 2px solid var(--light-blue);
14 | }
15 |
16 | th {
17 | border: 1px solid #eee;
18 | }
19 |
20 | th,
21 | td {
22 | vertical-align: top;
23 | text-align: left;
24 | padding: 8px;
25 | &.icons {
26 | > * {
27 | margin-right: 5px;
28 | cursor: pointer;
29 | }
30 | }
31 | }
32 |
33 | tr {
34 | border-bottom: 1px solid #ccc;
35 | }
36 |
37 | tr:nth-child(even) {
38 | background-color: #eee;
39 | }
40 | }
41 | .summary {
42 | margin-top: 2rem;
43 | display: flex;
44 | justify-content: space-between;
45 | align-items: start;
46 |
47 | .card {
48 | padding: 1rem;
49 | .text {
50 | display: flex;
51 | justify-content: space-between;
52 | align-items: center;
53 | h3 {
54 | color: var(--color-danger);
55 | }
56 | }
57 | button {
58 | margin-top: 5px;
59 | }
60 | }
61 | }
62 | }
63 | .count {
64 | display: flex;
65 | align-items: center;
66 | button {
67 | border: 1px solid var(--darkblue);
68 | }
69 | & > * {
70 | margin-right: 1rem;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/pages/orderHistory/OrderHistory.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { useNavigate } from "react-router-dom";
4 | import Loader from "../../components/loader/Loader";
5 | import useFetchCollection from "../../customHooks/useFetchCollection";
6 | import { selectUserID } from "../../redux/slice/authSlice";
7 | import { selectOrderHistory, STORE_ORDERS } from "../../redux/slice/orderSlice";
8 | import styles from "./OrderHistory.module.scss";
9 |
10 | const OrderHistory = () => {
11 | const { data, isLoading } = useFetchCollection("orders");
12 | const orders = useSelector(selectOrderHistory);
13 | const userID = useSelector(selectUserID);
14 |
15 | const dispatch = useDispatch();
16 | const navigate = useNavigate();
17 |
18 | useEffect(() => {
19 | dispatch(STORE_ORDERS(data));
20 | }, [dispatch, data]);
21 |
22 | const handleClick = (id) => {
23 | navigate(`/order-details/${id}`);
24 | };
25 |
26 | const filteredOrders = orders.filter((order) => order.userID === userID);
27 |
28 | return (
29 |
30 |
31 |
Your Order History
32 |
33 | Open an order to leave a Product Review
34 |
35 |
36 | <>
37 | {isLoading &&
}
38 |
39 | {filteredOrders.length === 0 ? (
40 |
No order found
41 | ) : (
42 |
43 |
44 |
45 | s/n |
46 | Date |
47 | Order ID |
48 | Order Amount |
49 | Order Status |
50 |
51 |
52 |
53 | {filteredOrders.map((order, index) => {
54 | const {
55 | id,
56 | orderDate,
57 | orderTime,
58 | orderAmount,
59 | orderStatus,
60 | } = order;
61 | return (
62 | handleClick(id)}>
63 | {index + 1} |
64 |
65 | {orderDate} at {orderTime}
66 | |
67 | {id} |
68 |
69 | {"$"}
70 | {orderAmount}
71 | |
72 |
73 |
80 | {orderStatus}
81 |
82 | |
83 |
84 | );
85 | })}
86 |
87 |
88 | )}
89 |
90 | >
91 |
92 |
93 | );
94 | };
95 |
96 | export default OrderHistory;
97 |
--------------------------------------------------------------------------------
/src/pages/orderHistory/OrderHistory.module.scss:
--------------------------------------------------------------------------------
1 | .table {
2 | padding: 5px;
3 | width: 100%;
4 | overflow-x: auto;
5 |
6 | table {
7 | border-collapse: collapse;
8 | width: 100%;
9 | font-size: 1.4rem;
10 |
11 | thead {
12 | border-top: 2px solid var(--light-blue);
13 | border-bottom: 2px solid var(--light-blue);
14 | }
15 |
16 | th {
17 | border: 1px solid #eee;
18 | }
19 |
20 | th,
21 | td {
22 | vertical-align: top;
23 | text-align: left;
24 | padding: 8px;
25 | &.icons {
26 | > * {
27 | margin-right: 5px;
28 | cursor: pointer;
29 | }
30 | }
31 | }
32 |
33 | tr {
34 | border-bottom: 1px solid #ccc;
35 | cursor: pointer;
36 | }
37 |
38 | tr:nth-child(even) {
39 | background-color: #eee;
40 | }
41 | .pending {
42 | color: orangered;
43 | font-weight: 500;
44 | }
45 | .delivered {
46 | color: green;
47 | font-weight: 500;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/redux/slice/authSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | isLoggedIn: false,
5 | email: null,
6 | useName: null,
7 | userID: null,
8 | };
9 |
10 | const authSlice = createSlice({
11 | name: "auth",
12 | initialState,
13 | reducers: {
14 | SET_ACTIVE_USER: (state, action) => {
15 | // console.log(action.payload);
16 | const { email, userName, userID } = action.payload;
17 | state.isLoggedIn = true;
18 | state.email = email;
19 | state.userName = userName;
20 | state.userID = userID;
21 | },
22 | REMOVE_ACTIVE_USER(state, action) {
23 | state.isLoggedIn = false;
24 | state.email = null;
25 | state.userName = null;
26 | state.userID = null;
27 | },
28 | },
29 | });
30 |
31 | export const { SET_ACTIVE_USER, REMOVE_ACTIVE_USER } = authSlice.actions;
32 |
33 | export const selectIsLoggedIn = (state) => state.auth.isLoggedIn;
34 | export const selectEmail = (state) => state.auth.email;
35 | export const selectUserName = (state) => state.auth.userName;
36 | export const selectUserID = (state) => state.auth.userID;
37 |
38 | export default authSlice.reducer;
39 |
--------------------------------------------------------------------------------
/src/redux/slice/cartSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { toast } from "react-toastify";
3 |
4 | const initialState = {
5 | cartItems: localStorage.getItem("cartItems")
6 | ? JSON.parse(localStorage.getItem("cartItems"))
7 | : [],
8 | cartTotalQuantity: 0,
9 | cartTotalAmount: 0,
10 | previousURL: "",
11 | };
12 |
13 | const cartSlice = createSlice({
14 | name: "cart",
15 | initialState,
16 | reducers: {
17 | ADD_TO_CART(state, action) {
18 | // console.log(action.payload);
19 | const productIndex = state.cartItems.findIndex(
20 | (item) => item.id === action.payload.id
21 | );
22 |
23 | if (productIndex >= 0) {
24 | // Item already exists in the cart
25 | // Increase the cartQuantity
26 | state.cartItems[productIndex].cartQuantity += 1;
27 | toast.info(`${action.payload.name} increased by one`, {
28 | position: "top-left",
29 | });
30 | } else {
31 | // Item doesn't exists in the cart
32 | // Add item to the cart
33 | const tempProduct = { ...action.payload, cartQuantity: 1 };
34 | state.cartItems.push(tempProduct);
35 | toast.success(`${action.payload.name} added to cart`, {
36 | position: "top-left",
37 | });
38 | }
39 | // save cart to LS
40 | localStorage.setItem("cartItems", JSON.stringify(state.cartItems));
41 | },
42 | DECREASE_CART(state, action) {
43 | console.log(action.payload);
44 | const productIndex = state.cartItems.findIndex(
45 | (item) => item.id === action.payload.id
46 | );
47 |
48 | if (state.cartItems[productIndex].cartQuantity > 1) {
49 | state.cartItems[productIndex].cartQuantity -= 1;
50 | toast.info(`${action.payload.name} decreased by one`, {
51 | position: "top-left",
52 | });
53 | } else if (state.cartItems[productIndex].cartQuantity === 1) {
54 | const newCartItem = state.cartItems.filter(
55 | (item) => item.id !== action.payload.id
56 | );
57 | state.cartItems = newCartItem;
58 | toast.success(`${action.payload.name} removed from cart`, {
59 | position: "top-left",
60 | });
61 | }
62 | localStorage.setItem("cartItems", JSON.stringify(state.cartItems));
63 | },
64 | REMOVE_FROM_CART(state, action) {
65 | const newCartItem = state.cartItems.filter(
66 | (item) => item.id !== action.payload.id
67 | );
68 |
69 | state.cartItems = newCartItem;
70 | toast.success(`${action.payload.name} removed from cart`, {
71 | position: "top-left",
72 | });
73 |
74 | localStorage.setItem("cartItems", JSON.stringify(state.cartItems));
75 | },
76 | CLEAR_CART(state, action) {
77 | state.cartItems = [];
78 | toast.info(`Cart cleared`, {
79 | position: "top-left",
80 | });
81 |
82 | localStorage.setItem("cartItems", JSON.stringify(state.cartItems));
83 | },
84 | CALCULATE_SUBTOTAL(state, action) {
85 | const array = [];
86 | state.cartItems.map((item) => {
87 | const { price, cartQuantity } = item;
88 | const cartItemAmount = price * cartQuantity;
89 | return array.push(cartItemAmount);
90 | });
91 | const totalAmount = array.reduce((a, b) => {
92 | return a + b;
93 | }, 0);
94 | state.cartTotalAmount = totalAmount;
95 | },
96 | CALCULATE_TOTAL_QUANTITY(state, action) {
97 | const array = [];
98 | state.cartItems.map((item) => {
99 | const { cartQuantity } = item;
100 | const quantity = cartQuantity;
101 | return array.push(quantity);
102 | });
103 | const totalQuantity = array.reduce((a, b) => {
104 | return a + b;
105 | }, 0);
106 | state.cartTotalQuantity = totalQuantity;
107 | },
108 | SAVE_URL(state, action) {
109 | console.log(action.payload);
110 | state.previousURL = action.payload;
111 | },
112 | },
113 | });
114 |
115 | export const {
116 | ADD_TO_CART,
117 | DECREASE_CART,
118 | REMOVE_FROM_CART,
119 | CLEAR_CART,
120 | CALCULATE_SUBTOTAL,
121 | CALCULATE_TOTAL_QUANTITY,
122 | SAVE_URL,
123 | } = cartSlice.actions;
124 |
125 | export const selectCartItems = (state) => state.cart.cartItems;
126 | export const selectCartTotalQuantity = (state) => state.cart.cartTotalQuantity;
127 | export const selectCartTotalAmount = (state) => state.cart.cartTotalAmount;
128 | export const selectPreviousURL = (state) => state.cart.previousURL;
129 |
130 | export default cartSlice.reducer;
131 |
--------------------------------------------------------------------------------
/src/redux/slice/checkoutSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | shippingAddress: {},
5 | billingAddress: {},
6 | };
7 |
8 | const checkoutSlice = createSlice({
9 | name: "checkout",
10 | initialState,
11 | reducers: {
12 | SAVE_SHIPPING_ADDRESS(state, action) {
13 | // console.log(action.payload);
14 | state.shippingAddress = action.payload;
15 | },
16 | SAVE_BILLING_ADDRESS(state, action) {
17 | // console.log(action.payload);
18 | state.billingAddress = action.payload;
19 | },
20 | },
21 | });
22 |
23 | export const { SAVE_BILLING_ADDRESS, SAVE_SHIPPING_ADDRESS } =
24 | checkoutSlice.actions;
25 |
26 | export const selectShippingAddress = (state) => state.checkout.shippingAddress;
27 | export const selectBillingAddress = (state) => state.checkout.billingAddress;
28 |
29 | export default checkoutSlice.reducer;
30 |
--------------------------------------------------------------------------------
/src/redux/slice/filterSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | filteredProducts: [],
5 | };
6 |
7 | const filterSlice = createSlice({
8 | name: "filter",
9 | initialState,
10 | reducers: {
11 | FILTER_BY_SEARCH(state, action) {
12 | const { products, search } = action.payload;
13 | const tempProducts = products.filter(
14 | (product) =>
15 | product.name.toLowerCase().includes(search.toLowerCase()) ||
16 | product.category.toLowerCase().includes(search.toLowerCase())
17 | );
18 |
19 | state.filteredProducts = tempProducts;
20 | },
21 | SORT_PRODUCTS(state, action) {
22 | const { products, sort } = action.payload;
23 | let tempProducts = [];
24 | if (sort === "latest") {
25 | tempProducts = products;
26 | }
27 |
28 | if (sort === "lowest-price") {
29 | tempProducts = products.slice().sort((a, b) => {
30 | return a.price - b.price;
31 | });
32 | }
33 |
34 | if (sort === "highest-price") {
35 | tempProducts = products.slice().sort((a, b) => {
36 | return b.price - a.price;
37 | });
38 | }
39 |
40 | if (sort === "a-z") {
41 | tempProducts = products.slice().sort((a, b) => {
42 | return a.name.localeCompare(b.name);
43 | });
44 | }
45 | if (sort === "z-a") {
46 | tempProducts = products.slice().sort((a, b) => {
47 | return b.name.localeCompare(a.name);
48 | });
49 | }
50 |
51 | state.filteredProducts = tempProducts;
52 | },
53 | FILTER_BY_CATEGORY(state, action) {
54 | const { products, category } = action.payload;
55 | let tempProducts = [];
56 | if (category === "All") {
57 | tempProducts = products;
58 | } else {
59 | tempProducts = products.filter(
60 | (product) => product.category === category
61 | );
62 | }
63 | state.filteredProducts = tempProducts;
64 | },
65 | FILTER_BY_BRAND(state, action) {
66 | const { products, brand } = action.payload;
67 | let tempProducts = [];
68 | if (brand === "All") {
69 | tempProducts = products;
70 | } else {
71 | tempProducts = products.filter((product) => product.brand === brand);
72 | }
73 | state.filteredProducts = tempProducts;
74 | },
75 | FILTER_BY_PRICE(state, action) {
76 | const { products, price } = action.payload;
77 | let tempProducts = [];
78 | tempProducts = products.filter((product) => product.price <= price);
79 |
80 | state.filteredProducts = tempProducts;
81 | },
82 | },
83 | });
84 |
85 | export const {
86 | FILTER_BY_SEARCH,
87 | SORT_PRODUCTS,
88 | FILTER_BY_CATEGORY,
89 | FILTER_BY_BRAND,
90 | FILTER_BY_PRICE,
91 | } = filterSlice.actions;
92 |
93 | export const selectFilteredProducts = (state) => state.filter.filteredProducts;
94 |
95 | export default filterSlice.reducer;
96 |
--------------------------------------------------------------------------------
/src/redux/slice/orderSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | orderHistory: [],
5 | totalOrderAmount: null,
6 | };
7 |
8 | const orderSlice = createSlice({
9 | name: "orders",
10 | initialState,
11 | reducers: {
12 | STORE_ORDERS(state, action) {
13 | state.orderHistory = action.payload;
14 | },
15 | CALC_TOTAL_ORDER_AMOUNT(state, action) {
16 | const array = [];
17 | state.orderHistory.map((item) => {
18 | const { orderAmount } = item;
19 | return array.push(orderAmount);
20 | });
21 | const totalAmount = array.reduce((a, b) => {
22 | return a + b;
23 | }, 0);
24 | state.totalOrderAmount = totalAmount;
25 | },
26 | },
27 | });
28 |
29 | export const { STORE_ORDERS, CALC_TOTAL_ORDER_AMOUNT } = orderSlice.actions;
30 |
31 | export const selectOrderHistory = (state) => state.orders.orderHistory;
32 | export const selectTotalOrderAmount = (state) => state.orders.totalOrderAmount;
33 |
34 | export default orderSlice.reducer;
35 |
--------------------------------------------------------------------------------
/src/redux/slice/productSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | products: [],
5 | minPrice: null,
6 | maxPrice: null,
7 | };
8 |
9 | const productSlice = createSlice({
10 | name: "product",
11 | initialState,
12 | reducers: {
13 | STORE_PRODUCTS(state, action) {
14 | // console.log(action.payload);
15 | state.products = action.payload.products;
16 | },
17 | GET_PRICE_RANGE(state, action) {
18 | const { products } = action.payload;
19 | const array = [];
20 | products.map((product) => {
21 | const price = product.price;
22 | return array.push(price);
23 | });
24 | const max = Math.max(...array);
25 | const min = Math.min(...array);
26 |
27 | state.minPrice = min;
28 | state.maxPrice = max;
29 | },
30 | },
31 | });
32 |
33 | export const { STORE_PRODUCTS, GET_PRICE_RANGE } = productSlice.actions;
34 |
35 | export const selectProducts = (state) => state.product.products;
36 | export const selectMinPrice = (state) => state.product.minPrice;
37 | export const selectMaxPrice = (state) => state.product.maxPrice;
38 |
39 | export default productSlice.reducer;
40 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore, combineReducers } from "@reduxjs/toolkit";
2 | import authReducer from "./slice/authSlice";
3 | import productReducer from "./slice/productSlice";
4 | import filterReducer from "./slice/filterSlice";
5 | import cartReducer from "./slice/cartSlice";
6 | import checkoutReducer from "./slice/checkoutSlice";
7 | import orderReducer from "./slice/orderSlice";
8 |
9 | const rootReducer = combineReducers({
10 | auth: authReducer,
11 | product: productReducer,
12 | filter: filterReducer,
13 | cart: cartReducer,
14 | checkout: checkoutReducer,
15 | orders: orderReducer,
16 | });
17 |
18 | const store = configureStore({
19 | reducer: rootReducer,
20 | middleware: (getDefaultMiddleware) =>
21 | getDefaultMiddleware({
22 | serializableCheck: false,
23 | }),
24 | });
25 |
26 | export default store;
27 |
--------------------------------------------------------------------------------