├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public └── vite.svg ├── server.js ├── src ├── App.jsx ├── assets │ ├── empty-cart.png │ └── hero3.png ├── components │ ├── Products │ │ ├── GridView.jsx │ │ ├── ListView.jsx │ │ ├── ProductDetails.jsx │ │ ├── ProductFilter.jsx │ │ └── ProductList.jsx │ ├── adminComponents │ │ ├── AddProducts.jsx │ │ ├── AdminHome.jsx │ │ ├── AdminOrderDetails.jsx │ │ ├── AdminSidebar.jsx │ │ ├── Orders.jsx │ │ └── ViewProducts.jsx │ ├── adminRoute │ │ └── AdminRoute.jsx │ ├── breadcrumbs │ │ └── Breadcrumbs.jsx │ ├── changeOrderStatus │ │ └── ChangeOrderStatus.jsx │ ├── chart │ │ └── chart.jsx │ ├── checkoutForm │ │ └── CheckoutForm.jsx │ ├── checkoutSummary │ │ └── CheckoutSummary.jsx │ ├── countdown │ │ └── Countdown.jsx │ ├── header │ │ └── Header.jsx │ ├── hero │ │ └── Hero.jsx │ ├── index.js │ ├── infoBox │ │ └── InfoBox.jsx │ ├── loader │ │ └── Loader.jsx │ ├── login │ │ └── Login.jsx │ ├── modal │ │ └── Modal.jsx │ ├── navbar │ │ └── Navbar.jsx │ ├── orderDetailsComponent │ │ └── OrderDetailsComponent.jsx │ ├── orderTable │ │ └── OrderTable.jsx │ ├── ordersComponent │ │ └── OrdersComponent.jsx │ ├── pagination │ │ └── Pagination.jsx │ ├── protectedRoute │ │ └── ProtectedRoute.jsx │ ├── register │ │ └── Register.jsx │ ├── review │ │ └── ReviewComponent.jsx │ ├── search │ │ └── Search.jsx │ └── steps │ │ └── Steps.jsx ├── firebase │ └── config.js ├── hooks │ ├── useCountdown.jsx │ ├── useFetchCollection.jsx │ └── useFetchDocument.jsx ├── index.css ├── main.jsx ├── pages │ ├── admin │ │ └── Admin.jsx │ ├── allProducts │ │ └── Allproducts.jsx │ ├── cart │ │ └── Cart.jsx │ ├── checkout │ │ ├── Checkout.jsx │ │ ├── CheckoutDetails.jsx │ │ ├── CheckoutSuccess.jsx │ │ └── stripe.css │ ├── contact │ │ └── Contact.jsx │ ├── home │ │ └── Home.jsx │ ├── index.js │ ├── notFound │ │ └── NotFound.jsx │ ├── orderDetails │ │ └── OrderDetails.jsx │ ├── orderHistory │ │ └── OrderHistory.jsx │ ├── resetPassword │ │ └── ResetPassword.jsx │ └── reviewProduct │ │ └── Review.jsx ├── redux │ ├── slice │ │ ├── authSlice.js │ │ ├── cartSlice.js │ │ ├── checkoutSlice.js │ │ ├── filterSlice.js │ │ ├── orderSlice.js │ │ └── productSlice.js │ └── store.js ├── stripeStyles.css └── utils │ ├── adminAddProductDefaultValues.js │ ├── adminProductCategories.js │ ├── formatPrice.js │ └── uniqueValues.js ├── tailwind.config.cjs ├── vite.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | dotenv 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fullstack-Ecommerce 2 | 3 | ## :moneybag: Eshop.com :moneybag: 4 | 5 | eShop.com is an e-Commerce website that enables users to shop through variety of products(electronic / household / fashion) , add a product to their cart, and checkout. A user can either register their own username and password or Sign in With Google, or they can simply use the "Guest Login" button to navigate the website without registering. 6 | 7 | ![download](https://user-images.githubusercontent.com/81632171/199007839-77a2f0cd-9b25-4dec-a141-30155fbc4a20.png) 8 | 9 | ## Summary 10 | 11 | - :star: [Website Link](#website-link) 12 | - :star: [Getting Started](#getting-started) 13 | - :star: [Prerequisites](#prerequisites) 14 | - :star: [Installing](#installing) 15 | - :star: [Built With](#built-with) 16 | - :star: [Software Developer](#software-developer) 17 | 18 | ## LIVE-Website-Link 19 | 20 | [EShop.com](https://eshop-firebase.vercel.app/) 21 | 22 | ## Getting-Started 23 | 24 | Feel free to fork the project and change it to your likings. Try it out by cloning the repo to your local machine or download the zip 25 | 26 | ## Prerequisites 27 | 28 | You need preferably the latest version of Chrome, and text editor. 29 | 30 | #### Go here for Chrome: https://www.google.com/chrome/ 31 | 32 | #### VSCode is my go to: https://code.visualstudio.com/ 33 | 34 | ## Installing 35 | 36 | To get started follow this guide: 37 | 38 | #### FOR DEVELOPMENT PURPOSES 39 | 40 | In your terminal clone repo to your local machine using git clone: 41 | 42 | ``` 43 | git clone https://github.com/kartikpavan/Fullstack-Ecommerce.git 44 | ``` 45 | 46 | Move to your newly cloned repo by entering the following in your terminal: 47 | 48 | ``` 49 | $ cd Fullstack-Ecommerce && yarn or npm install 50 | ``` 51 | 52 | To Run Project:- 53 | 54 | ``` 55 | $ yarn dev or npm run dev 56 | ``` 57 | 58 | To open all project files from terminal using VSCode just tpye and enter: 59 | 60 | ``` 61 | $ code . 62 | ``` 63 | 64 | ## Built With 65 | 66 | - React Js 67 | - Redux Toolkit 68 | - Firebase 69 | - Node.js 70 | - Express.js 71 | - Stripe 72 | - Chart Js 73 | - Email Js 74 | - Tailwind CSS 75 | - Daisy UI 76 | 77 | ## Software Developer 78 | 79 | - **Kartik Pavan** 80 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | E-Shop App 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-firebase-ecom", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "server": "nodemon server.js", 10 | "start": "node server" 11 | }, 12 | "dependencies": { 13 | "@emailjs/browser": "^3.7.0", 14 | "@reduxjs/toolkit": "^1.8.6", 15 | "@stripe/react-stripe-js": "^1.14.0", 16 | "@stripe/stripe-js": "^1.42.0", 17 | "chart.js": "^3.9.1", 18 | "cors": "^2.8.5", 19 | "daisyui": "^2.31.0", 20 | "dotenv": "^16.0.3", 21 | "express": "^4.18.2", 22 | "firebase": "^9.12.1", 23 | "react": "^18.2.0", 24 | "react-chartjs-2": "^4.3.1", 25 | "react-dom": "^18.2.0", 26 | "react-icons": "^4.6.0", 27 | "react-lazy-load-image-component": "^1.5.5", 28 | "react-redux": "^8.0.4", 29 | "react-router-dom": "^6.4.2", 30 | "react-star-rate": "^0.2.0", 31 | "react-toastify": "^9.0.8", 32 | "serve": "^14.0.1", 33 | "stripe": "^10.15.0" 34 | }, 35 | "devDependencies": { 36 | "@types/react": "^18.0.17", 37 | "@types/react-dom": "^18.0.6", 38 | "@vitejs/plugin-react": "^2.1.0", 39 | "autoprefixer": "^10.4.12", 40 | "postcss": "^8.4.18", 41 | "tailwindcss": "^3.1.8", 42 | "vite": "^3.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const express = require("express"); 3 | const cors = require("cors"); 4 | const path = require("path"); 5 | const app = express(); 6 | if (process.env.NODE_ENV === "production") { 7 | app.use(express.static("build")); 8 | app.get("*", (req, res) => { 9 | res.sendFile(path.resolve(__dirname, "build", "index.html")); 10 | }); 11 | } 12 | // This is your test secret API key. 13 | const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); 14 | 15 | app.use(express.json()); 16 | app.use(cors()); 17 | 18 | app.get("/", (req, res) => { 19 | res.send("Welcome to Eshop"); 20 | }); 21 | 22 | const newArray = []; 23 | const calculateOrderAmount = (items) => { 24 | items.map((item) => { 25 | const { price, qty } = item; 26 | const totalItemAmount = price * qty; 27 | return newArray.push(totalItemAmount); 28 | }); 29 | const totalCartAmount = newArray.reduce((total, curr) => total + curr, 0); 30 | return totalCartAmount * 100; 31 | }; 32 | 33 | app.post("/create-payment-intent", async (req, res) => { 34 | const { items, shippingAddress, description } = req.body; 35 | console.log(shippingAddress); 36 | // Create a PaymentIntent with the order amount and currency 37 | const paymentIntent = await stripe.paymentIntents.create({ 38 | amount: calculateOrderAmount(items), 39 | currency: "inr", 40 | automatic_payment_methods: { 41 | enabled: true, 42 | }, 43 | description, 44 | shipping: { 45 | address: { 46 | line1: shippingAddress.line1, 47 | line2: shippingAddress.line2, 48 | city: shippingAddress.city, 49 | country: shippingAddress.country, 50 | // pin_code: shippingAddress.pin_code, 51 | }, 52 | name: shippingAddress.name, 53 | phone: shippingAddress.phone, 54 | }, 55 | }); 56 | 57 | res.send({ 58 | clientSecret: paymentIntent.client_secret, 59 | }); 60 | }); 61 | 62 | const PORT = process.env.PORT || 4242; 63 | app.listen(PORT, () => console.log(`Node server listening on port ${PORT}!`)); 64 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom"; 2 | import { ToastContainer } from "react-toastify"; 3 | import { AdminRoute, Modal, Navbar, ProductDetails, ProtectedRoute } from "./components"; 4 | import { 5 | Admin, 6 | AllProducts, 7 | Cart, 8 | Checkout, 9 | CheckoutDetails, 10 | CheckoutSuccess, 11 | Contact, 12 | Home, 13 | NotFound, 14 | OrderDetails, 15 | OrderHistory, 16 | ResetPassword, 17 | Review, 18 | } from "./pages"; 19 | 20 | const App = () => { 21 | return ( 22 | <> 23 | 24 | 25 | 26 | } /> 27 | 31 | 32 | 33 | } 34 | /> 35 | } /> 36 | 40 | 41 | 42 | } 43 | /> 44 | } /> 45 | } /> 46 | } /> 47 | } /> 48 | } /> 49 | } /> 50 | } /> 51 | } /> 52 | {/* ADMIN ROUTES */} 53 | 57 | 58 | 59 | } 60 | /> 61 | 62 | {/* 404 routes */} 63 | } /> 64 | 65 | 66 | 67 | ); 68 | }; 69 | 70 | export default App; 71 | -------------------------------------------------------------------------------- /src/assets/empty-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartikpavan/Fullstack-Ecommerce/310237fc384d57ce03c83e69bd0f97c4b8b14e1b/src/assets/empty-cart.png -------------------------------------------------------------------------------- /src/assets/hero3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartikpavan/Fullstack-Ecommerce/310237fc384d57ce03c83e69bd0f97c4b8b14e1b/src/assets/hero3.png -------------------------------------------------------------------------------- /src/components/Products/GridView.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { formatPrice } from "../../utils/formatPrice"; 4 | import { FcSearch } from "react-icons/fc"; 5 | // lazy load 6 | import { LazyLoadImage } from "react-lazy-load-image-component"; 7 | import "react-lazy-load-image-component/src/effects/blur.css"; 8 | // redux 9 | import { useDispatch, useSelector } from "react-redux"; 10 | import { addToCart } from "../../redux/slice/cartSlice"; 11 | 12 | const GridView = ({ products }) => { 13 | const dispatch = useDispatch(); 14 | if (!products.length) { 15 | return

No Products Found

; 16 | } 17 | 18 | function add2CartFunction(product) { 19 | dispatch(addToCart(product)); 20 | } 21 | 22 | return ( 23 |
24 | {products.map((product) => { 25 | const { id, imageURL, name, price } = product; 26 | return ( 27 |
28 |
29 |
30 | 37 |
38 | Free Delivery 39 |
40 |
41 |

{name}

42 |

{formatPrice(price)}

43 |
44 |
45 | 46 | 49 | 50 | 56 |
57 |
58 |
59 | ); 60 | })} 61 |
62 | ); 63 | }; 64 | 65 | export default React.memo(GridView); 66 | -------------------------------------------------------------------------------- /src/components/Products/ListView.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { formatPrice } from "../../utils/formatPrice"; 3 | import { Link } from "react-router-dom"; 4 | // lazy load 5 | import { LazyLoadImage } from "react-lazy-load-image-component"; 6 | import "react-lazy-load-image-component/src/effects/blur.css"; 7 | // redux 8 | import { useDispatch, useSelector } from "react-redux"; 9 | import { addToCart } from "../../redux/slice/cartSlice"; 10 | 11 | const ListView = ({ products }) => { 12 | const dispatch = useDispatch(); 13 | 14 | if (!products.length) { 15 | return

No Products Found

; 16 | } 17 | 18 | function add2CartFunction(product) { 19 | dispatch(addToCart(product)); 20 | } 21 | 22 | return ( 23 |
24 | {products.map((product) => { 25 | return ( 26 |
27 |
28 | 35 |
36 | Free Delivery 37 |
38 |
39 |

{product.brand}

40 |

{product.name}

41 |

42 | {formatPrice(product.price)} 43 |

44 |

{product.description.slice(0, 150)}...

45 | 46 | 52 | 53 | 56 | 57 |
58 |
59 |
60 | ); 61 | })} 62 |
63 | ); 64 | }; 65 | 66 | export default React.memo(ListView); 67 | -------------------------------------------------------------------------------- /src/components/Products/ProductDetails.jsx: -------------------------------------------------------------------------------- 1 | import { doc, getDoc } from "firebase/firestore"; 2 | import Breadcrumbs from "../breadcrumbs/Breadcrumbs"; 3 | import { Link, useParams } from "react-router-dom"; 4 | import { formatPrice } from "../../utils/formatPrice"; 5 | import Loader from "../loader/Loader"; 6 | import ReviewComponent from "../review/ReviewComponent"; 7 | // Custom Hook 8 | import useFetchCollection from "../../hooks/useFetchCollection"; 9 | //Lazy Load 10 | import { LazyLoadImage } from "react-lazy-load-image-component"; 11 | import "react-lazy-load-image-component/src/effects/blur.css"; 12 | // Firebase 13 | import { useEffect, useState } from "react"; 14 | import { db } from "../../firebase/config"; 15 | // Redux 16 | import { useDispatch, useSelector } from "react-redux"; 17 | import { addToCart, decreaseCart, calculateTotalQuantity } from "../../redux/slice/cartSlice"; 18 | 19 | const ProductDetails = () => { 20 | // get cart items from redux store 21 | const { cartItems } = useSelector((store) => store.cart); 22 | const [product, setProduct] = useState({}); 23 | const [isLoading, setIsLoading] = useState(false); 24 | const { id } = useParams(); 25 | const dispatch = useDispatch(); 26 | 27 | //! fetch Review Collection 28 | const { data } = useFetchCollection("reviews"); 29 | 30 | // find the review which matches the current product 31 | const filteredReview = data.filter((item) => item.productId === id); 32 | 33 | //! fetch single product Document from products collection 34 | async function getSingleDocument() { 35 | setIsLoading(true); 36 | const docRef = doc(db, "products", id); 37 | const docSnap = await getDoc(docRef); 38 | if (docSnap.exists()) { 39 | setProduct(docSnap.data()); 40 | setIsLoading(false); 41 | } else { 42 | console.log("No such document!"); 43 | setIsLoading(false); 44 | } 45 | } 46 | // Fetching single document from firestore on initial component mount 47 | useEffect(() => { 48 | getSingleDocument(); 49 | }, []); 50 | 51 | // Add to cart 52 | function add2CartFunction(product) { 53 | dispatch(addToCart({ ...product, id })); 54 | dispatch(calculateTotalQuantity()); 55 | } 56 | // Decrease Qty 57 | function decreaseQty(product) { 58 | dispatch(decreaseCart({ ...product, id })); 59 | dispatch(calculateTotalQuantity()); 60 | } 61 | // Check if the item is already present in the cart or not 62 | let currentItem = cartItems.find((item) => item.id === id); 63 | 64 | return ( 65 | <> 66 | {isLoading && } 67 | 68 |
69 |

Product Details

70 | 71 | ← Back to All Products 72 | 73 |
74 |
75 | 82 |
83 |
84 |

{product.name}

85 |

86 | {formatPrice(product.price)} 87 |

88 |

{product.description}

89 |

90 | SKU : {id} 91 |

92 |

93 | Brand : {product.brand} 94 |

95 | {/* Button Group */} 96 | {cartItems.includes(currentItem) && ( 97 |
98 | 105 | 108 | 114 |
115 | )} 116 | 117 |
118 | 124 |
125 |
126 |
127 |
128 |
129 |

Reviews

130 |
131 | {!filteredReview.length ? ( 132 |

133 | 134 | Be the first one to review this product 135 | 136 |

137 | ) : ( 138 |
139 | {filteredReview.map((review, index) => { 140 | return ; 141 | })} 142 |
143 | )} 144 |
145 |
146 | 147 | ); 148 | }; 149 | 150 | export default ProductDetails; 151 | -------------------------------------------------------------------------------- /src/components/Products/ProductFilter.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | // Utilities 3 | import { getUniqueValues } from "../../utils/uniqueValues"; 4 | //Redux 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { filterByCategory, filterByBrand, filterByprice } from "../../redux/slice/filterSlice"; 7 | import { formatPrice } from "../../utils/formatPrice"; 8 | 9 | const ProductFilter = () => { 10 | const { products } = useSelector((store) => store.product); 11 | const { minPrice, maxPrice } = useSelector((store) => store.product); 12 | const dispatch = useDispatch(); 13 | 14 | const [category, setCategory] = useState("All"); 15 | const [brand, setBrand] = useState("All"); 16 | const [price, setPrice] = useState(maxPrice); 17 | 18 | // Getting new Category and brand Array 19 | const allCategories = getUniqueValues(products, "category"); 20 | const allBrands = getUniqueValues(products, "brand"); 21 | //! Categi 22 | const filterProducts = (c) => { 23 | setCategory(c); 24 | dispatch(filterByCategory({ products, category: c })); 25 | }; 26 | //! Brand 27 | useEffect(() => { 28 | dispatch(filterByBrand({ products, brand })); 29 | }, [dispatch, products, brand]); 30 | 31 | //!Price 32 | useEffect(() => { 33 | dispatch(filterByprice({ products, price })); 34 | }, [dispatch, products, price]); 35 | 36 | function clearFilter() { 37 | setCategory("All"); 38 | setBrand("All"); 39 | setPrice(maxPrice); 40 | } 41 | 42 | return ( 43 |
44 | {/* Categories */} 45 |
46 |

CATEGORIES

47 |
48 | {allCategories.map((c, index) => { 49 | return ( 50 | 62 | ); 63 | })} 64 |
65 |
66 | {/* Brand */} 67 |
68 |

BRAND

69 | 82 |
83 | {/* Price */} 84 |
85 |

PRICE

86 |

{formatPrice(price)}

87 | setPrice(e.target.value)} 94 | /> 95 |
96 |
97 | 100 |
101 |
102 | ); 103 | }; 104 | 105 | export default ProductFilter; 106 | -------------------------------------------------------------------------------- /src/components/Products/ProductList.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { ListView, GridView, Search, ProductFilter, Pagination } from "../../components"; 3 | import { BsFillGridFill, BsFilter } from "react-icons/bs"; 4 | import { MdOutlineSubject } from "react-icons/md"; 5 | // Redux 6 | import { filterBySearch, sortProducts } from "../../redux/slice/filterSlice"; 7 | import { useDispatch, useSelector } from "react-redux"; 8 | 9 | const ProductList = ({ products }) => { 10 | const { filteredProducts } = useSelector((store) => store.filter); 11 | const [grid, setGrid] = useState(true); 12 | const [search, setSearch] = useState(""); 13 | const [sort, setSort] = useState("latest"); 14 | // Pagination 15 | const [currentPage, setCurrentPage] = useState(1); 16 | const [productPerPage, setProductPerPage] = useState(9); 17 | // Scroll To Top 18 | const [bacToTop, setBackToTop] = useState(false); 19 | const dispatch = useDispatch(); 20 | 21 | //! Search 22 | useEffect(() => { 23 | dispatch(filterBySearch({ products, search })); 24 | }, [dispatch, products, search]); 25 | //! Sort 26 | useEffect(() => { 27 | dispatch(sortProducts({ products, sort })); 28 | }, [dispatch, products, sort]); 29 | 30 | useEffect(() => { 31 | // Scroll back to top 32 | const event = window.addEventListener("scroll", () => { 33 | if (pageYOffset > 400) { 34 | setBackToTop(true); 35 | } else { 36 | setBackToTop(false); 37 | } 38 | () => removeEventListener(event); 39 | }); 40 | }, []); 41 | 42 | function scrollToTop() { 43 | window.scrollTo({ 44 | top: 0, 45 | behavior: "smooth", 46 | }); 47 | } 48 | 49 | //get current product 50 | const indexOfLastProduct = currentPage * productPerPage; 51 | const indexOfFirstProduct = indexOfLastProduct - productPerPage; 52 | const currentProducts = filteredProducts.slice(indexOfFirstProduct, indexOfLastProduct); 53 | 54 | return ( 55 |
56 |
57 | {/* Grid or List layout */} 58 |
59 |
60 | setGrid(true)} 63 | className={` rounded-md p-1 ${grid ? "bg-neutral text-white" : null}`} 64 | /> 65 | setGrid(false)} 68 | className={` rounded-md p-1 ${grid ? null : "bg-neutral text-white"}`} 69 | /> 70 |
71 |

72 | {filteredProducts.length} - Products 73 | Found 74 |

75 |
76 | {/* Search Bar */} 77 | setSearch(e.target.value)} /> 78 | {/* Sorting List */} 79 |
80 | 81 | 92 |
93 | {/* Collapse for Filter */} 94 |
95 | 96 |
97 |

Show Filters

98 | 99 |
100 |
101 | {/* Filter Component */} 102 | 103 |
104 |
105 |
106 |
107 | {grid ? ( 108 | 109 | ) : ( 110 | 111 | )} 112 |
113 | {bacToTop && ( 114 |
115 | 121 |
122 | )} 123 | 129 |
130 | ); 131 | }; 132 | 133 | export default ProductList; 134 | -------------------------------------------------------------------------------- /src/components/adminComponents/AddProducts.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | import Loader from "../loader/Loader"; 4 | import { useNavigate, useParams } from "react-router-dom"; 5 | import { categories } from "../../utils/adminProductCategories"; 6 | import { defaultValues } from "../../utils/adminAddProductDefaultValues"; 7 | import { ref, uploadBytesResumable, getDownloadURL, deleteObject } from "firebase/storage"; 8 | import { collection, addDoc, Timestamp, setDoc, doc } from "firebase/firestore"; 9 | import { storage, db } from "../../firebase/config"; 10 | import { useSelector } from "react-redux"; 11 | 12 | //! Handle Input Changes 13 | const AddProducts = () => { 14 | const navigate = useNavigate(); 15 | const { id: paramsId } = useParams(); 16 | const { products: reduxProducts } = useSelector((store) => store.product); 17 | const productEdit = reduxProducts.find((item) => item.id === paramsId); 18 | const [product, setProduct] = useState(() => { 19 | return detectForm(paramsId, defaultValues, productEdit); 20 | }); 21 | const [uploadProgress, setUploadProgress] = useState(0); 22 | const [isLoading, setIsLoading] = useState(false); 23 | 24 | //! Check for Add or Edit 25 | function detectForm(paramsId, func1, func2) { 26 | if (paramsId === "ADD") return func1; 27 | return func2; 28 | } 29 | 30 | function handleInputChange(e) { 31 | const { name, value } = e.target; 32 | setProduct({ ...product, [name]: value }); 33 | } 34 | //! File Upload to FireStorage 35 | function handleImageChange(e) { 36 | const file = e.target.files[0]; 37 | const storageRef = ref(storage, `images/${Date.now()}${file.name}`); 38 | const uploadTask = uploadBytesResumable(storageRef, file); 39 | uploadTask.on( 40 | "state_changed", 41 | (snapshot) => { 42 | const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 43 | setUploadProgress(progress); 44 | }, 45 | (error) => { 46 | toast.error(error.code, error.message); 47 | }, 48 | () => { 49 | // Handle successful uploads on complete 50 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => { 51 | setProduct({ ...product, imageURL: downloadURL }); 52 | toast.success("File Uploaded Successfully"); 53 | }); 54 | } 55 | ); 56 | } 57 | //! Add Product to Firebase 58 | async function addProduct(e) { 59 | e.preventDefault(); 60 | setIsLoading(true); 61 | try { 62 | const docRef = await addDoc(collection(db, "products"), { 63 | name: product.name, 64 | imageURL: product.imageURL, 65 | price: Number(product.price), 66 | category: product.category, 67 | brand: product.brand, 68 | description: product.description, 69 | createdAt: Timestamp.now().toDate(), 70 | }); 71 | setUploadProgress(0); 72 | setProduct(defaultValues); 73 | setIsLoading(false); 74 | toast.success("Product added to Database Successfully"); 75 | navigate("/admin/all-products"); 76 | } catch (error) { 77 | console.log(error.message); 78 | toast.error("Something Went Wrong , Check Console"); 79 | setIsLoading(false); 80 | } 81 | } 82 | //! Edit Product 83 | async function editProduct(e) { 84 | e.preventDefault(); 85 | setIsLoading(true); 86 | // Check if the image is updated 87 | if (product.imageURL !== productEdit.imageURL) { 88 | // deleting image from database storage 89 | const storageRef = ref(storage, productEdit.imageURL); 90 | await deleteObject(storageRef); 91 | } 92 | try { 93 | await setDoc(doc(db, "products", paramsId), { 94 | name: product.name, 95 | imageURL: product.imageURL, 96 | price: Number(product.price), 97 | category: product.category, 98 | brand: product.brand, 99 | description: product.description, 100 | // Preserving created at 101 | createdAt: productEdit.createdAt, 102 | editedAt: Timestamp.now().toDate(), 103 | }); 104 | setUploadProgress(0); 105 | setProduct(defaultValues); 106 | setIsLoading(false); 107 | toast.success("Product Updated Successfully"); 108 | navigate("/admin/all-products"); 109 | } catch (error) { 110 | console.log(error.message); 111 | toast.error("Something Went Wrong , Check Console"); 112 | setIsLoading(false); 113 | } 114 | } 115 | 116 | //! Disable button until everything added to input fields 117 | const AllFieldsRequired = 118 | Boolean(product.brand) && 119 | Boolean(product.category) && 120 | Boolean(product.description) && 121 | Boolean(product.imageURL) && 122 | Boolean(product.name) && 123 | Boolean(product.name); 124 | 125 | return ( 126 | <> 127 | {isLoading && } 128 | 129 |
130 |

131 | {detectForm(paramsId, "Add New Product", "Edit Product")} 132 |

133 |
134 |
135 | 136 | 145 |
146 | 147 |
148 | 149 | 158 |
159 |
160 | 161 | 179 |
180 |
181 | 182 | 191 |
192 |
193 | 194 | 203 |
204 |
205 | 206 |
207 |
208 | 213 |
214 | 222 | {product.imageURL === "" ? null : ( 223 | 231 | )} 232 |
233 |
234 | 235 | 242 |
243 |
244 | 245 | ); 246 | }; 247 | 248 | export default AddProducts; 249 | -------------------------------------------------------------------------------- /src/components/adminComponents/AdminHome.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { InfoBox, Chart } from "../../components"; 3 | import { BiRupee } from "react-icons/bi"; 4 | import { FaCartArrowDown } from "react-icons/fa"; 5 | import { BsCart } from "react-icons/bs"; 6 | import { formatPrice } from "../../utils/formatPrice"; 7 | //redux 8 | import { useSelector, useDispatch } from "react-redux"; 9 | import { totalOrderAmount, storeOrders } from "../../redux/slice/orderSlice"; 10 | import useFetchCollection from "../../hooks/useFetchCollection"; 11 | 12 | const earningIcon = ; 13 | const productIcon = ; 14 | const orderIcon = ; 15 | 16 | const AdminHome = () => { 17 | const { data } = useFetchCollection("orders"); 18 | const { products } = useSelector((store) => store.product); 19 | const { orderHistory, totalAmount } = useSelector((store) => store.order); 20 | const dispatch = useDispatch(); 21 | 22 | useEffect(() => { 23 | dispatch(storeOrders(data)); 24 | dispatch(totalOrderAmount()); 25 | }, [dispatch, data]); 26 | 27 | return ( 28 |
29 |

Admin Home

30 |
31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default AdminHome; 43 | -------------------------------------------------------------------------------- /src/components/adminComponents/AdminOrderDetails.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useParams, Link } from "react-router-dom"; 3 | import Loader from "../../components/loader/Loader"; 4 | //firebase 5 | import useFetchDocument from "../../hooks/useFetchDocument"; 6 | import OrderDetailsComponent from "../../components/orderDetailsComponent/OrderDetailsComponent"; 7 | 8 | const AdminOrderDetails = () => { 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 | {order === null ? ( 20 | 21 | ) : ( 22 |
23 | 24 |
25 | )} 26 | 27 | ); 28 | }; 29 | 30 | export default AdminOrderDetails; 31 | -------------------------------------------------------------------------------- /src/components/adminComponents/AdminSidebar.jsx: -------------------------------------------------------------------------------- 1 | import { RiAdminLine } from "react-icons/ri"; 2 | import { useSelector } from "react-redux"; 3 | import { NavLink } from "react-router-dom"; 4 | 5 | const AdminSidebar = () => { 6 | const { userName } = useSelector((store) => store.auth); 7 | 8 | // Active link class 9 | let activeStyle = { 10 | borderRight: "5px solid #181a2a", 11 | display: "block", 12 | // padding: "0.5rem 0", 13 | }; 14 | return ( 15 |
16 |
17 | 18 |

{userName}

19 |
20 |
21 | (isActive ? activeStyle : null)}> 22 | Home 23 | 24 |
25 |
26 | (isActive ? activeStyle : null)} 29 | > 30 | All Products 31 | 32 |
33 |
34 | (isActive ? activeStyle : null)} 37 | > 38 | Add Products 39 | 40 |
41 |
42 | (isActive ? activeStyle : null)} 45 | > 46 | Orders 47 | 48 |
49 |
50 | ); 51 | }; 52 | 53 | export default AdminSidebar; 54 | -------------------------------------------------------------------------------- /src/components/adminComponents/Orders.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import useFetchCollection from "../../hooks/useFetchCollection"; 3 | import Loader from "../../components/loader/Loader"; 4 | import { useNavigate } from "react-router-dom"; 5 | // Redux 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import { storeOrders } from "../../redux/slice/orderSlice"; 8 | 9 | import OrdersComponent from "../ordersComponent/OrdersComponent"; 10 | 11 | const Orders = () => { 12 | const { data, isLoading } = useFetchCollection("orders"); 13 | const { orderHistory } = useSelector((store) => store.order); 14 | const { userId } = useSelector((store) => store.auth); 15 | const dispatch = useDispatch(); 16 | const navigate = useNavigate(); 17 | 18 | useEffect(() => { 19 | dispatch(storeOrders(data)); 20 | }, [dispatch, data]); 21 | 22 | return ( 23 | <> 24 | {isLoading && } 25 |

ALL ORDERS

26 |
27 | 28 |
29 | 30 | ); 31 | }; 32 | 33 | export default Orders; 34 | -------------------------------------------------------------------------------- /src/components/adminComponents/ViewProducts.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import Loader from "../loader/Loader"; 3 | import { toast } from "react-toastify"; 4 | import { formatPrice } from "../../utils/formatPrice"; 5 | import { BiEdit, BiTrash } from "react-icons/bi"; 6 | import { Link } from "react-router-dom"; 7 | import { Search } from "../../components"; 8 | import useFetchCollection from "../../hooks/useFetchCollection"; 9 | import { doc, deleteDoc } from "firebase/firestore"; 10 | import { ref, deleteObject } from "firebase/storage"; 11 | import { db, storage } from "../../firebase/config"; 12 | import { useDispatch, useSelector } from "react-redux"; 13 | import { storeProducts } from "../../redux/slice/productSlice"; 14 | import { filterBySearch } from "../../redux/slice/filterSlice"; 15 | import { LazyLoadImage } from "react-lazy-load-image-component"; 16 | import "react-lazy-load-image-component/src/effects/blur.css"; 17 | 18 | const ViewProducts = () => { 19 | const [search, setSearch] = useState(""); 20 | const dispatch = useDispatch(); 21 | 22 | //! Fetching Products from collection using Custom Hook 23 | const { data, isLoading } = useFetchCollection("products"); 24 | const { filteredProducts } = useSelector((store) => store.filter); 25 | const { products } = useSelector((store) => store.product); 26 | 27 | useEffect(() => { 28 | dispatch(storeProducts({ products: data })); 29 | }, [dispatch, data]); 30 | 31 | //! Search 32 | useEffect(() => { 33 | dispatch(filterBySearch({ products: data, search })); 34 | }, [dispatch, data, search]); 35 | 36 | //! Delete single product 37 | const deleteSingleProduct = async (id, imageURL) => { 38 | try { 39 | // deleting a document from product collection 40 | await deleteDoc(doc(db, "products", id)); 41 | // deleting image from database storage 42 | const storageRef = ref(storage, imageURL); 43 | await deleteObject(storageRef); 44 | toast.info("Product deleted successfully"); 45 | } catch (error) { 46 | toast.error(error.message); 47 | console.log(error.message); 48 | } 49 | }; 50 | return ( 51 | <> 52 | {isLoading && } 53 |

All Products

54 | {products.length && ( 55 |
56 |
57 | {filteredProducts.length} products found 58 |
59 |
60 | )} 61 | setSearch(e.target.value)} /> 62 |
63 | {filteredProducts.length === 0 ? ( 64 |

NO PRODUCTS FOUND

65 | ) : ( 66 |
67 | 68 | {/* TABLE HEAD */} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | {/* TABLE BODY */} 80 | 81 | {filteredProducts?.map((p, index) => { 82 | const { id, name, category, price, imageURL } = p; 83 | return ( 84 | 85 | 86 | 100 | 101 | 102 | 103 | 120 | 121 | ); 122 | })} 123 | 124 |
ImageNameCategoryPriceOptions
{index + 1} 87 |
88 | 98 |
99 |
{name}{category}{formatPrice(price)} 104 |
105 | 106 | 107 | 108 | 118 |
119 |
125 |
126 | )} 127 |
128 | 129 | ); 130 | }; 131 | 132 | export default ViewProducts; 133 | -------------------------------------------------------------------------------- /src/components/adminRoute/AdminRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useSelector } from "react-redux"; 4 | 5 | const AdminRoute = ({ children }) => { 6 | const { email } = useSelector((store) => store.auth); 7 | if (email === import.meta.env.VITE_ADMIN_KEY) return children; 8 | return ( 9 |
10 |

PERMISSION DENIED

11 |

This page can only be viewed by admin.

12 | 13 | ← Take me back home 14 | 15 |
16 | ); 17 | }; 18 | 19 | export const AdminOnlyLink = ({ children }) => { 20 | const { email } = useSelector((store) => store.auth); 21 | if (email === import.meta.env.VITE_ADMIN_KEY) return children; 22 | return null; 23 | }; 24 | 25 | export default AdminRoute; 26 | -------------------------------------------------------------------------------- /src/components/breadcrumbs/Breadcrumbs.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, NavLink } from "react-router-dom"; 3 | 4 | const Breadcrumbs = ({ type, checkout, stripe }) => { 5 | const activeLink = ({ isActive }) => (isActive ? "text-secondary-content " : null); 6 | 7 | return ( 8 |
9 |
10 | 11 | Home / 12 | 13 | 14 | Products 15 | 16 | 17 | {type && ( 18 | 19 | / {type} 20 | 21 | )} 22 | {checkout && ( 23 | 24 | / {checkout} 25 | 26 | )} 27 | {stripe && ( 28 | 29 | / {stripe} 30 | 31 | )} 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default Breadcrumbs; 38 | -------------------------------------------------------------------------------- /src/components/changeOrderStatus/ChangeOrderStatus.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | import Loader from "../loader/Loader"; 4 | import { useNavigate } from "react-router-dom"; 5 | // firebase 6 | import { doc, setDoc, Timestamp } from "firebase/firestore"; 7 | import { db } from "../../firebase/config"; 8 | 9 | const ChangeOrderStatus = ({ order, orderId }) => { 10 | const [status, setStatus] = useState(""); 11 | const [isLoading, setIsloading] = useState(false); 12 | const navigate = useNavigate(); 13 | 14 | const changeStatus = (e, orderId) => { 15 | e.preventDefault(); 16 | setIsloading(true); 17 | const orderDetails = { 18 | userId: order.userId, 19 | email: order.email, 20 | orderDate: order.orderDate, 21 | orderTime: order.orderTime, 22 | orderAmount: order.orderAmount, 23 | orderStatus: status, 24 | cartItems: order.cartItems, 25 | shippingAddress: order.shippingAddress, 26 | createdAt: order.createdAt, 27 | editedAt: Timestamp.now().toDate(), 28 | }; 29 | try { 30 | setDoc(doc(db, "orders", orderId), orderDetails); 31 | setIsloading(false); 32 | toast.success(`order status changed to ${status}`); 33 | navigate("/admin/orders"); 34 | } catch (error) { 35 | toast.error(error.message); 36 | console.log(error); 37 | setIsloading(false); 38 | } 39 | }; 40 | 41 | return ( 42 | <> 43 | {isLoading && } 44 |
45 |

Update Order Status

46 |
changeStatus(e, orderId)} className="form-control"> 47 | 58 | 61 |
62 |
63 | 64 | ); 65 | }; 66 | 67 | export default ChangeOrderStatus; 68 | -------------------------------------------------------------------------------- /src/components/chart/chart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useFetchCollection from "../../hooks/useFetchCollection"; 3 | 4 | import { 5 | Chart as ChartJS, 6 | CategoryScale, 7 | LinearScale, 8 | BarElement, 9 | Title, 10 | Tooltip, 11 | Legend, 12 | } from "chart.js"; 13 | import { Bar } from "react-chartjs-2"; 14 | import { useSelector } from "react-redux"; 15 | 16 | ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); 17 | 18 | const options = { 19 | responsive: true, 20 | plugins: { 21 | legend: { 22 | position: "top", 23 | }, 24 | title: { 25 | display: true, 26 | text: "Order Status", 27 | }, 28 | }, 29 | }; 30 | 31 | const chart = () => { 32 | const { orderHistory } = useSelector((store) => store.order); 33 | // Create new Array of order status 34 | const filteredOrders = orderHistory.map((item) => item.orderStatus); 35 | 36 | // Count the occurances of order status(s) 37 | const getOrderCount = (arr, value) => { 38 | return arr.filter((item) => item === value).length; 39 | }; 40 | 41 | const placed = getOrderCount(filteredOrders, "Order Placed"); 42 | const processing = getOrderCount(filteredOrders, "Processing..."); 43 | const shipped = getOrderCount(filteredOrders, "Item(s) Shipped"); 44 | const delivered = getOrderCount(filteredOrders, "Item(s) Delivered"); 45 | 46 | const data = { 47 | labels: ["Order Places", "Processing", "Shipped", "Delivered"], 48 | datasets: [ 49 | { 50 | label: "Order Count", 51 | data: [placed, shipped, processing, delivered], 52 | backgroundColor: "#191a3ed6", 53 | }, 54 | ], 55 | }; 56 | return ; 57 | }; 58 | 59 | export default chart; 60 | -------------------------------------------------------------------------------- /src/components/checkoutForm/CheckoutForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { 3 | PaymentElement, 4 | useStripe, 5 | useElements, 6 | } from "@stripe/react-stripe-js"; 7 | import CheckoutSummary from "../checkoutSummary/CheckoutSummary"; 8 | import Breadcrumbs from "../breadcrumbs/Breadcrumbs"; 9 | import Header from "../header/Header"; 10 | import { toast } from "react-toastify"; 11 | import { useNavigate } from "react-router-dom"; 12 | // firebase 13 | import { collection, addDoc, Timestamp } from "firebase/firestore"; 14 | import { db } from "../../firebase/config"; 15 | //redux 16 | import { useSelector, useDispatch } from "react-redux"; 17 | import { clearCart } from "../../redux/slice/cartSlice"; 18 | import Loader from "../loader/Loader"; 19 | 20 | const CheckoutForm = () => { 21 | const stripe = useStripe(); 22 | const elements = useElements(); 23 | 24 | const [message, setMessage] = useState(null); 25 | const [isLoading, setIsLoading] = useState(false); 26 | 27 | const navigate = useNavigate(); 28 | const dispatch = useDispatch(); 29 | const { email, userId } = useSelector((store) => store.auth); 30 | const { cartItems, totalAmount } = useSelector((store) => store.cart); 31 | const { shippingAddress } = useSelector((store) => store.checkout); 32 | 33 | const saveOrder = () => { 34 | const date = new Date().toDateString(); 35 | const time = new Date().toLocaleTimeString(); 36 | const orderDetails = { 37 | userId, 38 | email, 39 | orderDate: date, 40 | orderTime: time, 41 | orderAmount: totalAmount, 42 | orderStatus: "Order Placed", 43 | cartItems, 44 | shippingAddress, 45 | createdAt: Timestamp.now().toDate(), 46 | }; 47 | try { 48 | addDoc(collection(db, "orders"), orderDetails); 49 | dispatch(clearCart()); 50 | } catch (error) { 51 | toast.error(error.message); 52 | } 53 | }; 54 | 55 | const handleSubmit = async (e) => { 56 | e.preventDefault(); 57 | setMessage(null); 58 | if (!stripe || !elements) { 59 | return; 60 | } 61 | setIsLoading(true); 62 | const confirmPayment = await stripe 63 | .confirmPayment({ 64 | elements, 65 | confirmParams: { 66 | // Make sure to change this to your payment completion page 67 | return_url: "http://localhost:5173/checkout-success", 68 | }, 69 | redirect: "if_required", 70 | }) 71 | .then((res) => { 72 | if (res.error) { 73 | setMessage(res.error.message); 74 | toast.error(res.error.message); 75 | return; 76 | } 77 | if (res.paymentIntent) { 78 | if (res.paymentIntent.status === "succeeded") { 79 | setIsLoading(false); 80 | toast.success("Payment Successful"); 81 | saveOrder(); 82 | navigate("/checkout-success", { replace: true }); 83 | } 84 | } 85 | }); 86 | setIsLoading(false); 87 | }; 88 | 89 | useEffect(() => { 90 | if (!stripe) { 91 | return; 92 | } 93 | const clientSecret = new URLSearchParams(window.location.search).get( 94 | "payment_intent_client_secret" 95 | ); 96 | if (!clientSecret) { 97 | return; 98 | } 99 | }, [stripe]); 100 | 101 | return ( 102 | <> 103 |
104 |
105 |
106 |
107 | 108 |
109 |
110 |

Stripe Checkout

111 |
112 | 113 | 127 | {/* Show any error or success messages */} 128 | {message &&
{message}
} 129 | 130 |
131 |
132 |
133 | 134 | ); 135 | }; 136 | 137 | export default CheckoutForm; 138 | -------------------------------------------------------------------------------- /src/components/checkoutSummary/CheckoutSummary.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { formatPrice } from "../../utils/formatPrice"; 4 | 5 | const CheckoutSummary = () => { 6 | const { cartItems, totalQuantity, totalAmount } = useSelector( 7 | (store) => store.cart 8 | ); 9 | return ( 10 | <> 11 |

Checkout Summary

12 |
13 |

14 | Cart Item(s): {totalQuantity}{" "} 15 |

16 |
17 |

Subtotal:

18 |

19 | {formatPrice(totalAmount)} 20 |

21 |
22 | {cartItems.map((item) => { 23 | const { id, name, price, qty } = item; 24 | return ( 25 |
29 |

30 | {name} 31 |

32 |

Quantity: {qty}

33 |

Unit Price : {formatPrice(price)}

34 |

Total Price: {formatPrice(price * qty)}

35 |
36 | ); 37 | })} 38 |
39 | 40 | ); 41 | }; 42 | 43 | export default CheckoutSummary; 44 | -------------------------------------------------------------------------------- /src/components/countdown/Countdown.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Countdown = () => { 4 | return ( 5 |
6 |
7 | 8 | 9 | 10 | days 11 |
12 |
13 | 14 | 15 | 16 | hours 17 |
18 |
19 | 20 | 21 | 22 | min 23 |
24 |
25 | 26 | 27 | 28 | sec 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Countdown; 35 | -------------------------------------------------------------------------------- /src/components/header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Header = ({ text }) => { 4 | return ( 5 |
6 |
7 |

{text}

8 |
9 |
10 | ); 11 | }; 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /src/components/hero/Hero.jsx: -------------------------------------------------------------------------------- 1 | import hero from "../../assets/hero3.png"; 2 | import { Link } from "react-router-dom"; 3 | import { TbArrowNarrowRight } from "react-icons/tb"; 4 | import { useEffect, useState } from "react"; 5 | const tags = ["Mobiles", "Electronics", "Bags", "Clothes", "Jwellery"]; 6 | 7 | let currentIndex = 0; 8 | const Hero = () => { 9 | const [tagName, setTagName] = useState(""); 10 | function updateCountdown() { 11 | const currentItem = tags[currentIndex]; 12 | setTagName(currentItem); 13 | currentIndex = (currentIndex + 1) % tags.length; 14 | setTimeout(updateCountdown, 2000); 15 | } 16 | 17 | useEffect(() => { 18 | updateCountdown(); 19 | }, []); 20 | 21 | return ( 22 | //
23 | //
24 | // 28 | //
29 | //

Limited Time Only

30 | //

Fashion

31 | //

32 | // Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi 33 | // exercitationem quasi. 34 | //

35 | // 39 | // Shop Now 40 | // 41 | // 42 | //
43 | //
44 | //
45 | <> 46 |
47 |
48 |
49 |
50 |
51 |

52 | Best place to choose
your{" "} 53 | 54 | {tagName} 55 | 56 |

57 | 58 |

59 | Lorem ipsum dolor sit amet, consectetur adipisicing 60 | elit. Porro beatae error laborum ab amet sunt 61 | recusandae? Reiciendis natus perspiciatis optio. 62 |

63 | 64 | 65 | 68 | 69 |
70 |
71 | 72 |
73 | Catalogue-pana.svg 78 |
79 |
80 |
81 |
82 | 83 | ); 84 | }; 85 | 86 | export default Hero; 87 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as GridView } from "./Products/GridView"; 2 | export { default as ListView } from "./Products/ListView"; 3 | export { default as ProductDetails } from "./Products/ProductDetails"; 4 | export { default as ProductFilter } from "./Products/ProductFilter"; 5 | export { default as ProductList } from "./Products/ProductList"; 6 | export { default as AddProducts } from "./adminComponents/AddProducts"; 7 | export { default as AdminHome } from "./adminComponents/AdminHome"; 8 | export { default as AdminOrderDetails } from "./adminComponents/AdminOrderDetails"; 9 | export { default as AdminSidebar } from "./adminComponents/AdminSidebar"; 10 | export { default as Orders } from "./adminComponents/Orders"; 11 | export { default as ViewProducts } from "./adminComponents/ViewProducts"; 12 | export { default as AdminRoute } from "./adminRoute/AdminRoute"; 13 | export { default as Breadcrumbs } from "./breadcrumbs/Breadcrumbs"; 14 | export { default as Chart } from "./chart/chart"; 15 | export { default as CheckoutForm } from "./checkoutForm/CheckoutForm"; 16 | export { default as CheckoutSummary } from "./checkoutSummary/CheckoutSummary"; 17 | export { default as Header } from "./header/Header"; 18 | export { default as Hero } from "./hero/Hero"; 19 | export { default as InfoBox } from "./infoBox/InfoBox"; 20 | export { default as Login } from "./login/Login"; 21 | export { default as Modal } from "./modal/Modal"; 22 | export { default as Navbar } from "./navbar/Navbar"; 23 | export { default as OrderDetailsComponent } from "./orderDetailsComponent/OrderDetailsComponent"; 24 | export { default as OrderTable } from "./orderTable/OrderTable"; 25 | export { default as OrdersComponent } from "./ordersComponent/OrdersComponent"; 26 | export { default as Pagination } from "./pagination/Pagination"; 27 | export { default as ProtectedRoute } from "./protectedRoute/ProtectedRoute"; 28 | export { default as Register } from "./register/Register"; 29 | export { default as ReviewComponent } from "./review/ReviewComponent"; 30 | export { default as Search } from "./search/Search"; 31 | export { default as Steps } from "./steps/Steps"; 32 | -------------------------------------------------------------------------------- /src/components/infoBox/InfoBox.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const InfoBox = ({ color, title, count, icon }) => { 4 | return ( 5 |
6 |

{title}

7 |
8 |

{count}

9 |

{icon}

10 |
11 |
12 | ); 13 | }; 14 | 15 | export default InfoBox; 16 | -------------------------------------------------------------------------------- /src/components/loader/Loader.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | 3 | const Loader = () => { 4 | return ReactDOM.createPortal( 5 |
6 |
7 |

Please wait , This might take a while

8 |
9 |
10 |
, 11 | document.getElementById("loader") 12 | ); 13 | // return ( 14 | //
15 | //
16 | //
17 | // ); 18 | }; 19 | 20 | export default Loader; 21 | -------------------------------------------------------------------------------- /src/components/login/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { AiFillEye, AiFillEyeInvisible } from "react-icons/ai"; 3 | import { FcGoogle } from "react-icons/fc"; 4 | import { Link, useNavigate } from "react-router-dom"; 5 | import { toast } from "react-toastify"; 6 | import "react-toastify/dist/ReactToastify.css"; 7 | import Loader from "../loader/Loader"; 8 | //Firebase 9 | import { GoogleAuthProvider, signInWithEmailAndPassword, signInWithPopup } from "firebase/auth"; 10 | import { auth } from "../../firebase/config"; 11 | 12 | const Login = () => { 13 | const [email, setEmail] = useState(""); 14 | const [password, setPassword] = useState(""); 15 | const [showPassword, setShowPassword] = useState(false); 16 | const [isLoading, setIsLoading] = useState(false); 17 | const navigate = useNavigate(); 18 | 19 | //! Test / Guest Account Login 20 | //* Currently this feature is disabled due to spam messages reported by the ADMIN 21 | const testLogin = (e) => { 22 | e.preventDefault(); 23 | // document.getElementById("my-modal-69").checked = true; 24 | // setInfoModalOpen(true); 25 | document.getElementById("my-modal-4").checked = false; 26 | 27 | let testEmail = import.meta.env.VITE_TEST_EMAIL; 28 | let testPass = import.meta.env.VITE_TEST_PASSWORD; 29 | setIsLoading(true); 30 | signInWithEmailAndPassword(auth, testEmail, testPass) 31 | .then((userCredential) => { 32 | const user = userCredential.user; 33 | toast.success("Login Successful"); 34 | setIsLoading(false); 35 | // document.getElementById("my-modal-4").checked = false; 36 | navigate("/"); 37 | }) 38 | .catch((error) => { 39 | toast.error(error.code, error.message); 40 | setIsLoading(false); 41 | }); 42 | }; 43 | 44 | const handleSubmit = (e) => { 45 | e.preventDefault(); 46 | // Close dialog box when Login Btn is clicked immediately 47 | document.getElementById("my-modal-4").checked = false; 48 | 49 | //* Custom User login 50 | setIsLoading(true); 51 | signInWithEmailAndPassword(auth, email, password) 52 | .then((userCredential) => { 53 | const user = userCredential.user; 54 | toast.success("Login Successful"); 55 | setIsLoading(false); 56 | navigate("/"); 57 | }) 58 | .catch((error) => { 59 | toast.error(error.code, error.message); 60 | setIsLoading(false); 61 | }); 62 | 63 | setEmail(""); 64 | setPassword(""); 65 | }; 66 | 67 | //* Login with Google 68 | const provider = new GoogleAuthProvider(); 69 | const googleSignIn = () => { 70 | setIsLoading(true); 71 | document.getElementById("my-modal-4").checked = false; 72 | signInWithPopup(auth, provider) 73 | .then((result) => { 74 | const user = result.user; 75 | toast.success("Login Successful"); 76 | setIsLoading(false); 77 | navigate("/"); 78 | }) 79 | .catch((error) => { 80 | toast.error(error.code, error.message); 81 | setIsLoading(false); 82 | }); 83 | }; 84 | 85 | const AllFieldsRequired = Boolean(email) && Boolean(password); 86 | 87 | return ( 88 | <> 89 | {isLoading && } 90 |
91 |
92 |
93 |

Welcome back

94 |
95 | 96 | Sign in with google 97 |
98 |
or login with email
99 |
100 |
101 | 102 | setEmail(e.target.value)} 108 | /> 109 |
110 |
111 |
112 | 113 | (document.getElementById("my-modal-4").checked = false)} 117 | > 118 | Forget Password? 119 | 120 |
121 | setPassword(e.target.value)} 127 | /> 128 | setShowPassword((prev) => !prev)}> 129 | {showPassword ? ( 130 | 131 | ) : ( 132 | 137 | )} 138 | 139 |
140 |
141 | 144 | 145 | {/* The button to open modal */} 146 | {/* */} 153 | 154 | {/* Put this part before tag */} 155 | 156 | 172 |
173 |
174 |
175 |
176 |
177 | 178 | ); 179 | }; 180 | 181 | export default Login; 182 | -------------------------------------------------------------------------------- /src/components/modal/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Login from "../login/Login"; 3 | import Register from "../register/Register"; 4 | const Modal = () => { 5 | const [isLogin, setIsLogin] = useState(true); 6 | 7 | return ( 8 | <> 9 | 10 | 27 | 28 | ); 29 | }; 30 | 31 | export default Modal; 32 | -------------------------------------------------------------------------------- /src/components/navbar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { AiOutlineShoppingCart } from "react-icons/ai"; 3 | import { Link, NavLink, useNavigate } from "react-router-dom"; 4 | import { toast } from "react-toastify"; 5 | import "react-toastify/dist/ReactToastify.css"; 6 | import { AdminOnlyLink } from "../adminRoute/AdminRoute"; 7 | // firebase 8 | import { onAuthStateChanged, signOut } from "firebase/auth"; 9 | import { auth } from "../../firebase/config"; 10 | //Redux 11 | import { useDispatch, useSelector } from "react-redux"; 12 | import { removeActiveUser, setActiveUser } from "../../redux/slice/authSlice"; 13 | import { calculateSubtotal, calculateTotalQuantity } from "../../redux/slice/cartSlice"; 14 | import { formatPrice } from "../../utils/formatPrice"; 15 | 16 | const Navbar = () => { 17 | const { isUserLoggedIn, userName } = useSelector((store) => store.auth); 18 | const { totalAmount, totalQuantity, cartItems } = useSelector((store) => store.cart); 19 | const [displayName, setDisplayName] = useState(""); 20 | const navigate = useNavigate(); 21 | const dispatch = useDispatch(); 22 | 23 | //* Monitor currently signed USER 24 | useEffect(() => { 25 | onAuthStateChanged(auth, (user) => { 26 | if (user) { 27 | if (displayName == null) { 28 | setDisplayName(user.email.split("@")[0]); 29 | } 30 | dispatch( 31 | setActiveUser({ 32 | email: user.email, 33 | userName: user.displayName ? user.displayName : displayName, 34 | userId: user.uid, 35 | }) 36 | ); 37 | } else { 38 | setDisplayName(""); 39 | dispatch(removeActiveUser()); 40 | } 41 | }); 42 | }, []); 43 | 44 | function logOutUser() { 45 | signOut(auth) 46 | .then(() => { 47 | toast.success("User Signed Out "); 48 | navigate("/"); 49 | }) 50 | .catch((error) => { 51 | toast.error(error.code, error.message); 52 | }); 53 | } 54 | let activeStyle = { 55 | borderBottom: "2px solid white", 56 | }; 57 | 58 | useEffect(() => { 59 | dispatch(calculateTotalQuantity()); 60 | dispatch(calculateSubtotal()); 61 | }, [dispatch, cartItems]); 62 | 63 | return ( 64 | <> 65 | 182 | 183 |
184 | ADMIN 185 | 186 | VIEW DASHBOARD 187 | 188 |
189 |
190 | {/*
191 |

Sale end in

192 | 193 |
*/} 194 | 195 | ); 196 | }; 197 | 198 | export default Navbar; 199 | -------------------------------------------------------------------------------- /src/components/orderDetailsComponent/OrderDetailsComponent.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { formatPrice } from "../../utils/formatPrice"; 3 | 4 | import ChangeOrderStatus from "../changeOrderStatus/ChangeOrderStatus"; 5 | // import Steps from "../steps/Steps"; 6 | import { OrderTable, Steps } from "../../components"; 7 | 8 | const OrderDetailsComponent = ({ order, admin, user, orderId }) => { 9 | return ( 10 | <> 11 |
12 |
13 | {/* Order Details */} 14 |
15 |
16 |

17 | Order Details 18 |

19 |

20 | Order ID : {order.id} 21 |

22 |

23 | Order Amount : 24 | 25 | {formatPrice(order.orderAmount)} 26 | 27 |

28 |

29 | Order Status : 30 | 37 | {order.orderStatus} 38 | 39 |

40 |
41 | {/* Steps for order traacking only for user */} 42 | {user && } 43 | {admin && ( 44 |
45 | {/* Recipient Name */} 46 |

47 | Recipient Name : 48 | {order.shippingAddress.name} 49 |

50 | {/* Phone Number */} 51 |

52 | Phone :{order.shippingAddress.phone} 53 |

54 | {/* Address */} 55 |

56 | Shipping Address : 57 | 58 | {order.shippingAddress.line1}, {order.shippingAddress.line2}, 59 | {order.shippingAddress.city},{order.shippingAddress.country} 60 | 61 |

62 |
63 | )} 64 |
65 | {/* Update order Status */} 66 | {admin && } 67 |
68 |
69 |
70 |
71 | {admin ? ( 72 | 73 | ← Back to All Orders 74 | 75 | ) : ( 76 | 77 | ← Back to All Orders 78 | 79 | )} 80 |
81 | 82 | 83 |
84 | 85 | ); 86 | }; 87 | 88 | export default OrderDetailsComponent; 89 | -------------------------------------------------------------------------------- /src/components/orderTable/OrderTable.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | // lazy load 3 | import { LazyLoadImage } from "react-lazy-load-image-component"; 4 | import "react-lazy-load-image-component/src/effects/blur.css"; 5 | import { formatPrice } from "../../utils/formatPrice"; 6 | 7 | const OrderTable = ({ user, order }) => { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 | {user && } 15 | 16 | 17 | 18 | {order.cartItems.map((product, index) => { 19 | const { id: productId, name, price, imageURL, qty } = product; 20 | return ( 21 | 22 | 47 | {user && ( 48 | 56 | )} 57 | 58 | ); 59 | })} 60 | 61 |
ProductActions
23 | 24 | 34 |
{name}
35 |
36 | Qty: 37 | {qty} 38 |
39 |
40 | Total: 41 | 42 | {formatPrice(price * qty)} 43 | 44 |
45 | 46 |
49 | 53 | Write a Review 54 | 55 |
62 |
63 | ); 64 | }; 65 | 66 | export default OrderTable; 67 | -------------------------------------------------------------------------------- /src/components/ordersComponent/OrdersComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { formatPrice } from "../../utils/formatPrice"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | const OrdersComponent = ({ orders, user, admin }) => { 6 | const navigate = useNavigate(); 7 | 8 | function handleUserClick(orderId) { 9 | navigate(`/order-details/${orderId}`); 10 | } 11 | function handleAdminClick(orderId) { 12 | navigate(`/admin/order-details/${orderId}`); 13 | } 14 | 15 | return ( 16 |
17 | {!orders.length ? ( 18 |

No Orders found

19 | ) : ( 20 |
21 |

22 | Open order to ( 23 | {admin ? ( 24 | Change Order Status 25 | ) : ( 26 | Track Order Status 27 | )} 28 | ) 29 |

30 | 31 | {orders.map((order, index) => { 32 | const { id, orderDate, orderAmount, orderStatus, email } = order; 33 | return ( 34 |
{ 38 | user ? handleUserClick(id) : handleAdminClick(id); 39 | }} 40 | > 41 |
42 |
43 |
44 |

45 | ORDER PLACED :
{" "} 46 | {orderDate} 47 |

48 |

49 | SHIP TO :
{" "} 50 | 51 | {email.split("@")[0]} 52 | 53 |

54 |
55 | 56 |

57 | TOTAL : 58 | 59 | {formatPrice(orderAmount)} 60 | 61 |

62 |
63 |
64 |
65 |

66 | ID: {id} 67 |

68 |

69 | Status:
{" "} 70 | 77 | {orderStatus} 78 | 79 |

80 |
81 |
82 | ); 83 | })} 84 |
85 | )} 86 |
87 | ); 88 | }; 89 | 90 | export default OrdersComponent; 91 | -------------------------------------------------------------------------------- /src/components/pagination/Pagination.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Pagination = ({ productPerPage, currentPage, setCurrentPage, totalProducts }) => { 4 | function prevPage() { 5 | setCurrentPage((prev) => { 6 | if (prev <= 1) { 7 | return prev; 8 | } else { 9 | return prev - 1; 10 | } 11 | }); 12 | } 13 | function nextPage() { 14 | setCurrentPage((prev) => { 15 | if (prev >= Math.ceil(totalProducts / productPerPage)) { 16 | return prev; 17 | } else { 18 | return prev + 1; 19 | } 20 | }); 21 | } 22 | return ( 23 |
24 |
25 | 28 | 29 | 32 |
33 |

34 | Page {currentPage} of 35 | {Math.ceil(totalProducts / productPerPage)} 36 |

37 |
38 | ); 39 | }; 40 | 41 | export default Pagination; 42 | -------------------------------------------------------------------------------- /src/components/protectedRoute/ProtectedRoute.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { Navigate } from "react-router-dom"; 3 | 4 | const ProtectedRoute = ({ children }) => { 5 | const { isUserLoggedIn } = useSelector((store) => store.auth); 6 | if (isUserLoggedIn) { 7 | return children; 8 | } 9 | return ; 10 | }; 11 | 12 | export default ProtectedRoute; 13 | -------------------------------------------------------------------------------- /src/components/register/Register.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { AiFillEye, AiFillEyeInvisible } from "react-icons/ai"; 3 | import { toast } from "react-toastify"; 4 | import "react-toastify/dist/ReactToastify.css"; 5 | import Loader from "../loader/Loader"; 6 | //Firebase 7 | import { createUserWithEmailAndPassword } from "firebase/auth"; 8 | import { useNavigate } from "react-router-dom"; 9 | import { auth } from "../../firebase/config"; 10 | 11 | const Register = () => { 12 | const [email, setEmail] = useState(""); 13 | const [password, setPassword] = useState(""); 14 | const [confirmPassword, setConfirmPassword] = useState(""); 15 | const [showPassword, setShowPassword] = useState(false); 16 | const [isLoading, setIsLoading] = useState(false); 17 | const navigate = useNavigate(); 18 | 19 | const handleSubmit = (e) => { 20 | e.preventDefault(); 21 | if (password !== confirmPassword) { 22 | toast.error("Password Did not match"); 23 | } 24 | //* USER REGISTER 25 | setIsLoading(true); 26 | createUserWithEmailAndPassword(auth, email, password) 27 | .then(() => { 28 | toast.success("Registration Successful"); 29 | setIsLoading(false); 30 | document.getElementById("my-modal-4").checked = false; 31 | navigate("/"); 32 | }) 33 | .catch((error) => { 34 | toast.error(error.code, error.message); 35 | setIsLoading(false); 36 | }); 37 | 38 | setEmail(""); 39 | setPassword(""); 40 | setConfirmPassword(""); 41 | }; 42 | 43 | const AllFieldsRequired = Boolean(email) && Boolean(password) && Boolean(confirmPassword); 44 | 45 | return ( 46 | <> 47 | {isLoading && } 48 |
49 |
50 |
51 |

Create a new Account

52 | 53 |
54 |
55 | 56 | setEmail(e.target.value)} 62 | /> 63 |
64 |
65 |
66 | 67 |
68 | setPassword(e.target.value)} 74 | /> 75 | setShowPassword((prev) => !prev)}> 76 | {showPassword ? ( 77 | 78 | ) : ( 79 | 84 | )} 85 | 86 |
87 |
88 | 89 | setConfirmPassword(e.target.value)} 95 | /> 96 |
97 | 98 |
99 | 102 |
103 |
104 |
105 |
106 |
107 | 108 | ); 109 | }; 110 | 111 | export default Register; 112 | -------------------------------------------------------------------------------- /src/components/review/ReviewComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import StarsRating from "react-star-rate"; 3 | 4 | const ReviewComponent = ({ review }) => { 5 | const { userName, rating, review: comments, reviewDate } = review; 6 | return ( 7 |
8 |
9 |
10 | dp 15 |
16 |

17 | {userName || "Anonymous"} 18 |

19 |

{reviewDate}

20 |
21 |
22 | 23 |
24 |

{comments}

25 |
26 | ); 27 | }; 28 | 29 | export default ReviewComponent; 30 | -------------------------------------------------------------------------------- /src/components/search/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BiSearch } from "react-icons/bi"; 3 | const Search = ({ value, onChange }) => { 4 | return ( 5 |
6 |
7 | 14 | 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Search; 23 | -------------------------------------------------------------------------------- /src/components/steps/Steps.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Steps = ({ order }) => { 4 | return ( 5 | <> 6 | {order.orderStatus === "Order Placed" && ( 7 |
    8 |
  • 9 |
     Order-Placed 
    10 |
  • 11 |
  • 12 |
     Processing 
    13 |
  • 14 |
  • 15 |
     Order-Shipped 
    16 |
  • 17 |
  • 18 |
     Order-Delivered 
    19 |
  • 20 |
21 | )} 22 | {order.orderStatus === "Processing..." && ( 23 |
    24 |
  • 25 |
     Order-Placed 
    26 |
  • 27 |
  • 28 |
     Processing 
    29 |
  • 30 |
  • 31 |
     Order-Shipped 
    32 |
  • 33 |
  • 34 |
     Order-Delivered 
    35 |
  • 36 |
37 | )} 38 | {order.orderStatus === "Item(s) Shipped" && ( 39 |
    40 |
  • 41 |
     Order-Placed 
    42 |
  • 43 |
  • 44 |
     Processing 
    45 |
  • 46 |
  • 47 |
     Order-Shipped 
    48 |
  • 49 |
  • 50 |
     Order-Delivered 
    51 |
  • 52 |
53 | )} 54 | {order.orderStatus === "Item(s) Delivered" && ( 55 |
    56 |
  • 57 |
     Order-Placed 
    58 |
  • 59 |
  • 60 |
     Processing 
    61 |
  • 62 |
  • 63 |
     Order-Shipped 
    64 |
  • 65 |
  • 66 |
     Order-Delivered 
    67 |
  • 68 |
69 | )} 70 | 71 | ); 72 | }; 73 | 74 | export default Steps; 75 | -------------------------------------------------------------------------------- /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 | export const firebaseConfig = { 7 | apiKey: import.meta.env.VITE_API_KEY, 8 | authDomain: import.meta.env.VITE_AUTH_DOMAIN, 9 | projectId: import.meta.env.VITE_PROJECT_ID, 10 | storageBucket: import.meta.env.VITE_STORAGE_BUCKET, 11 | messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID, 12 | appId: import.meta.env.VITE_APP_ID, 13 | }; 14 | 15 | // Initialize Firebase 16 | const app = initializeApp(firebaseConfig); 17 | export const auth = getAuth(app); 18 | export const db = getFirestore(app); 19 | export const storage = getStorage(app); 20 | export default app; 21 | -------------------------------------------------------------------------------- /src/hooks/useCountdown.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useEffect } from "react"; 3 | 4 | const useCountdown = () => { 5 | const now = new Date().getTime(); 6 | const deadline = new Date("12 jan,2023,00:15:00").getTime(); 7 | 8 | const [countDown, setCountDown] = useState(deadline - now); 9 | 10 | useEffect(() => { 11 | const timer = setInterval(() => { 12 | setCountDown(deadline - now); 13 | }, 1000); 14 | return () => clearInterval(timer); 15 | }, [deadline]); 16 | 17 | return getRealTime(countDown); 18 | }; 19 | 20 | const getRealTime = (countDown) => { 21 | const days = Math.floor(countDown / (1000 * 60 * 60 * 24)); 22 | const hours = Math.floor((countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 23 | const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60)); 24 | const seconds = Math.floor((countDown % (1000 * 60)) / 1000); 25 | 26 | return [days, hours, minutes, seconds]; 27 | }; 28 | 29 | export { useCountdown }; 30 | -------------------------------------------------------------------------------- /src/hooks/useFetchCollection.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | //Firebase 4 | import { collection, query, orderBy, onSnapshot } from "firebase/firestore"; 5 | import { db } from "../firebase/config"; 6 | 7 | const useFetchCollection = (collectionName) => { 8 | const [data, setData] = useState([]); 9 | const [isLoading, setIsLoading] = useState(false); 10 | 11 | function fetchProductCollection() { 12 | setIsLoading(true); 13 | try { 14 | const docRef = collection(db, collectionName); 15 | const q = query(docRef, orderBy("createdAt", "desc")); 16 | onSnapshot(q, (querySnapshot) => { 17 | const allData = []; 18 | querySnapshot.forEach((doc) => { 19 | allData.push({ id: doc.id, ...doc.data() }); 20 | }); 21 | setData(allData); 22 | setIsLoading(false); 23 | }); 24 | } catch (error) { 25 | toast.error(error.code, error.message); 26 | setIsLoading(false); 27 | } 28 | } 29 | useEffect(() => { 30 | fetchProductCollection(); 31 | }, []); 32 | return { data, isLoading }; 33 | }; 34 | 35 | export default useFetchCollection; 36 | -------------------------------------------------------------------------------- /src/hooks/useFetchDocument.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | //Firebase 3 | import { doc, getDoc } from "firebase/firestore"; 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 | if (docSnap.exists()) { 13 | const obj = { 14 | id: documentId, 15 | ...docSnap.data(), 16 | }; 17 | setDocument(obj); 18 | } else { 19 | console.log("No such document exist!"); 20 | } 21 | }; 22 | // Fetching single document from firestore on initial component mount 23 | useEffect(() => { 24 | getDocument(); 25 | }, []); 26 | 27 | return { document }; 28 | }; 29 | 30 | export default useFetchDocument; 31 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Caveat:wght@400;700&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;700&display=swap"); 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | body { 9 | font-family: "Rubik", sans-serif; 10 | } 11 | 12 | .page { 13 | height: calc(100vh - 10rem); 14 | } 15 | 16 | .logo { 17 | font-family: "Caveat", cursive; 18 | } 19 | 20 | td { 21 | font-weight: 600; 22 | } 23 | 24 | /* LOADING SPINNER CSS */ 25 | 26 | .overlay { 27 | height: 100vh; 28 | width: 100vw; 29 | position: fixed; 30 | display: grid; 31 | place-items: center; 32 | background: rgba(0, 0, 0, 0.6); 33 | z-index: 100; 34 | } 35 | 36 | .loader { 37 | border: 16px solid #f3f3f3; 38 | border-radius: 50%; 39 | border-top: 16px solid #181a2a; 40 | width: 80px; 41 | height: 80px; 42 | animation: spin 2s linear infinite; 43 | } 44 | 45 | @keyframes spin { 46 | 0% { 47 | transform: rotate(0deg); 48 | } 49 | 100% { 50 | transform: rotate(360deg); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | //redux 7 | import { Provider } from "react-redux"; 8 | import { store } from "./redux/store"; 9 | 10 | ReactDOM.createRoot(document.getElementById("root")).render( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/pages/admin/Admin.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import { 4 | AddProducts, 5 | AdminHome, 6 | AdminOrderDetails, 7 | AdminSidebar, 8 | Orders, 9 | ViewProducts, 10 | } from "../../components"; 11 | 12 | const Admin = () => { 13 | return ( 14 | <> 15 |
16 |
17 |
18 | 19 |
20 |
21 | 22 | } /> 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | 28 |
29 |
30 |
31 | 32 | ); 33 | }; 34 | 35 | export default Admin; 36 | -------------------------------------------------------------------------------- /src/pages/allProducts/Allproducts.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Breadcrumbs, ProductFilter, ProductList } from "../../components"; 3 | import Loader from "../../components/loader/Loader"; 4 | 5 | // custom Hook 6 | import useFetchCollection from "../../hooks/useFetchCollection"; 7 | // Redux 8 | import { useSelector, useDispatch } from "react-redux"; 9 | import { storeProducts, getPriceRange } from "../../redux/slice/productSlice"; 10 | 11 | const Allproducts = () => { 12 | const { data, isLoading } = useFetchCollection("products"); 13 | const dispatch = useDispatch(); 14 | 15 | useEffect(() => { 16 | dispatch(storeProducts({ products: data })); 17 | dispatch(getPriceRange({ products: data })); 18 | }, [dispatch, data]); 19 | 20 | const { products } = useSelector((store) => store.product); 21 | 22 | return ( 23 | <> 24 | {isLoading && } 25 |
26 | 27 |
28 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | ); 38 | }; 39 | 40 | export default Allproducts; 41 | -------------------------------------------------------------------------------- /src/pages/cart/Cart.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { Breadcrumbs } from "../../components"; 4 | import { formatPrice } from "../../utils/formatPrice"; 5 | import { BiTrash } from "react-icons/bi"; 6 | import emptyCart from "../../assets/empty-cart.png"; 7 | // Redux 8 | import { useDispatch, useSelector } from "react-redux"; 9 | import { 10 | addToCart, 11 | decreaseCart, 12 | removeCartItem, 13 | clearCart, 14 | calculateSubtotal, 15 | calculateTotalQuantity, 16 | } from "../../redux/slice/cartSlice"; 17 | // lazy load 18 | import { LazyLoadImage } from "react-lazy-load-image-component"; 19 | import "react-lazy-load-image-component/src/effects/blur.css"; 20 | 21 | const Cart = () => { 22 | const { cartItems, totalAmount, totalQuantity } = useSelector((store) => store.cart); 23 | const { isUserLoggedIn } = useSelector((store) => store.auth); 24 | const dispatch = useDispatch(); 25 | //! increase cart item Qty 26 | const increaseQty = (item) => { 27 | dispatch(addToCart(item)); 28 | }; 29 | //! Decrease Cart Item Qty 30 | const decreaseQty = (item) => { 31 | dispatch(decreaseCart(item)); 32 | }; 33 | //! Remove Single Item 34 | const removeItem = (item) => { 35 | dispatch(removeCartItem(item)); 36 | }; 37 | 38 | useEffect(() => { 39 | dispatch(calculateSubtotal()); 40 | dispatch(calculateTotalQuantity()); 41 | }, [dispatch, cartItems]); 42 | 43 | return ( 44 |
45 | 46 |
47 | {!cartItems.length ? ( 48 |
49 | empty-Cart 50 |

51 | Looks like you have not added anything to your cart{" "} 52 |

53 |
54 | 55 | Click here to Explore more 56 | 57 |
58 | ) : ( 59 |
60 |

Shopping Cart

61 |
62 |
63 | 64 | {/* Head */} 65 | 66 | 67 | 68 | 69 | 70 | 71 | {/* Body */} 72 | 73 | {cartItems.map((item, index) => { 74 | const { imageURL, name, price, qty, id } = item; 75 | return ( 76 | 77 | 134 | 135 | 142 | 143 | ); 144 | })} 145 | 146 |
ItemActions
78 | 82 | 92 | 93 | 94 |
95 | 96 |

97 | {name} 98 |

99 | 100 | 101 |

102 | {formatPrice(price)} 103 |

104 |

Qty:

105 |
106 | 114 | 117 | 125 |
126 |

127 | Total: 128 | 129 | {formatPrice(price * qty)} 130 | 131 |

132 |
133 |
136 | removeItem(item)} 140 | /> 141 |
147 |
148 | 149 |
150 | 151 | ← Continue Shopping 152 | 153 |

154 | Cart items : {totalQuantity}{" "} 155 |

156 |
157 |

Subtotal

158 |

159 | {formatPrice(totalAmount)} 160 |

161 |
162 |

163 | Tax and Shipping calculated at checkout 164 |

165 | {isUserLoggedIn ? ( 166 | 167 | Checkout 168 | 169 | ) : ( 170 | 176 | )} 177 |
178 |
179 |
180 | )} 181 |
182 |
183 | ); 184 | }; 185 | 186 | export default Cart; 187 | -------------------------------------------------------------------------------- /src/pages/checkout/Checkout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Loader from "../../components/loader/Loader"; 3 | import { CheckoutForm } from "../../components"; 4 | import { loadStripe } from "@stripe/stripe-js"; 5 | import { Elements } from "@stripe/react-stripe-js"; 6 | import "./stripe.css"; 7 | // Make sure to call loadStripe outside of a component’s render to avoid 8 | // recreating the Stripe object on every render. 9 | const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLIC_KEY); 10 | //Redux 11 | import { useSelector, useDispatch } from "react-redux"; 12 | import { calculateSubtotal, calculateTotalQuantity } from "../../redux/slice/cartSlice"; 13 | import { formatPrice } from "../../utils/formatPrice"; 14 | 15 | const Checkout = () => { 16 | // Redux states 17 | const { cartItems, totalQuantity, totalAmount } = useSelector((store) => store.cart); 18 | const { shippingAddress, billingAddress } = useSelector((store) => store.checkout); 19 | const { email } = useSelector((store) => store.auth); 20 | const dispatch = useDispatch(); 21 | useEffect(() => { 22 | dispatch(calculateSubtotal()); 23 | dispatch(calculateTotalQuantity()); 24 | }, [dispatch, cartItems]); 25 | 26 | // local States 27 | const [clientSecret, setClientSecret] = useState(""); 28 | 29 | const description = `Payment of ${formatPrice(totalAmount)} from ${email}`; 30 | useEffect(() => { 31 | // Create PaymentIntent as soon as the page loads 32 | fetch("https://ecom-stripe-server.onrender.com/create-payment-intent", { 33 | method: "POST", 34 | headers: { "Content-Type": "application/json" }, 35 | body: JSON.stringify({ 36 | items: cartItems, 37 | userEmail: email, 38 | shippingAddress, 39 | billingAddress, 40 | description, 41 | }), 42 | }) 43 | .then((res) => res.json()) 44 | .then((data) => setClientSecret(data.clientSecret)); 45 | }, []); 46 | 47 | const appearance = { 48 | theme: "stripe", 49 | }; 50 | const options = { 51 | clientSecret, 52 | appearance, 53 | }; 54 | return ( 55 |
56 | {/* {!clientSecret &&

{message}

} */} 57 | {!clientSecret && } 58 |
59 | {clientSecret && ( 60 | 61 | 62 | 63 | )} 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default Checkout; 70 | -------------------------------------------------------------------------------- /src/pages/checkout/CheckoutDetails.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Breadcrumbs, CheckoutSummary } from "../../components"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | //Redux 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import { saveShippingAddress, saveBillingAddress } from "../../redux/slice/checkoutSlice"; 8 | 9 | const defaultValues = { 10 | name: "", 11 | line1: "", 12 | line2: "", 13 | city: "", 14 | pin_code: "", 15 | country: "", 16 | phone: "", 17 | }; 18 | 19 | const CheckoutDetails = () => { 20 | const [shippingAddress, setShippingAddress] = useState(defaultValues); 21 | const dispatch = useDispatch(); 22 | const navigate = useNavigate(); 23 | 24 | function handleShipping(e) { 25 | const { name, value } = e.target; 26 | setShippingAddress({ ...shippingAddress, [name]: value }); 27 | } 28 | 29 | function handleSubmit(e) { 30 | e.preventDefault(); 31 | dispatch(saveShippingAddress(shippingAddress)); 32 | dispatch(saveBillingAddress(shippingAddress)); 33 | navigate("/checkout"); 34 | setShippingAddress(defaultValues); 35 | } 36 | 37 | const AllFieldsRequired = 38 | Boolean(shippingAddress.name) && 39 | Boolean(shippingAddress.line1) && 40 | Boolean(shippingAddress.line2) && 41 | Boolean(shippingAddress.city) && 42 | Boolean(shippingAddress.country) && 43 | Boolean(shippingAddress.pin_code) && 44 | Boolean(shippingAddress.phone); 45 | 46 | return ( 47 |
48 | 49 |
50 |
51 |
52 |

Shipping Address

53 |
54 |
55 | 58 | 66 |
67 |
68 | 71 | 79 |
80 |
81 | 84 | 92 |
93 | 94 |
95 | 96 | 104 |
105 |
106 | 107 | 115 |
116 |
117 | 118 | 126 |
127 |
128 | 129 | 137 |
138 | 145 |
146 |
147 |
148 | 149 |
150 |
151 |
152 |
153 | ); 154 | }; 155 | 156 | export default CheckoutDetails; 157 | -------------------------------------------------------------------------------- /src/pages/checkout/CheckoutSuccess.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { MdOutlineVerified } from "react-icons/md"; 4 | const CheckoutSuccess = () => { 5 | return ( 6 |
7 |
8 | 9 |

10 | Hooray! Your order was confirmed 11 |

12 |

Thank you for shopping with us.

13 | 14 | View Order Status 15 | 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default CheckoutSuccess; 22 | -------------------------------------------------------------------------------- /src/pages/checkout/stripe.css: -------------------------------------------------------------------------------- 1 | #payment-form { 2 | width: 30vw; 3 | min-width: 500px; 4 | align-self: center; 5 | box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1), 0px 2px 5px 0px rgba(50, 50, 93, 0.1), 6 | 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07); 7 | border-radius: 7px; 8 | padding: 40px; 9 | } 10 | 11 | #payment-message { 12 | color: rgb(105, 115, 134); 13 | font-size: 16px; 14 | line-height: 20px; 15 | padding-top: 12px; 16 | text-align: center; 17 | } 18 | 19 | #payment-element { 20 | margin-bottom: 24px; 21 | } 22 | 23 | /* button:hover { 24 | filter: contrast(115%); 25 | } 26 | 27 | /* button:disabled { 28 | opacity: 0.5; 29 | cursor: default; 30 | } */ 31 | */ 32 | 33 | /* spinner/processing state, errors */ 34 | .spinner, 35 | .spinner:before, 36 | .spinner:after { 37 | border-radius: 50%; 38 | } 39 | 40 | .spinner { 41 | color: #ffffff; 42 | font-size: 22px; 43 | text-indent: -99999px; 44 | margin: 0px auto; 45 | position: relative; 46 | width: 20px; 47 | height: 20px; 48 | box-shadow: inset 0 0 0 2px; 49 | -webkit-transform: translateZ(0); 50 | -ms-transform: translateZ(0); 51 | transform: translateZ(0); 52 | } 53 | 54 | .spinner:before, 55 | .spinner:after { 56 | position: absolute; 57 | content: ""; 58 | } 59 | 60 | .spinner:before { 61 | width: 10.4px; 62 | height: 20.4px; 63 | background: #5469d4; 64 | border-radius: 20.4px 0 0 20.4px; 65 | top: -0.2px; 66 | left: -0.2px; 67 | -webkit-transform-origin: 10.4px 10.2px; 68 | transform-origin: 10.4px 10.2px; 69 | -webkit-animation: loading 2s infinite ease 1.5s; 70 | animation: loading 2s infinite ease 1.5s; 71 | } 72 | 73 | .spinner:after { 74 | width: 10.4px; 75 | height: 10.2px; 76 | background: #5469d4; 77 | border-radius: 0 10.2px 10.2px 0; 78 | top: -0.1px; 79 | left: 10.2px; 80 | -webkit-transform-origin: 0px 10.2px; 81 | transform-origin: 0px 10.2px; 82 | -webkit-animation: loading 2s infinite ease; 83 | animation: loading 2s infinite ease; 84 | } 85 | 86 | @keyframes loading { 87 | 0% { 88 | -webkit-transform: rotate(0deg); 89 | transform: rotate(0deg); 90 | } 91 | 100% { 92 | -webkit-transform: rotate(360deg); 93 | transform: rotate(360deg); 94 | } 95 | } 96 | 97 | /* @media only screen and (max-width: 600px) { 98 | form { 99 | width: 80vw; 100 | min-width: initial; 101 | } 102 | } */ 103 | -------------------------------------------------------------------------------- /src/pages/contact/Contact.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import { Header } from "../../components"; 3 | import { CiPhone } from "react-icons/ci"; 4 | import { AiOutlineMail, AiOutlineTwitter } from "react-icons/ai"; 5 | import emailjs from "@emailjs/browser"; 6 | import { toast } from "react-toastify"; 7 | 8 | const Contact = () => { 9 | const formRef = useRef(); 10 | const [loading, setLoading] = useState(true); 11 | 12 | const sendEmail = (e) => { 13 | e.preventDefault(); 14 | setLoading(true); 15 | emailjs 16 | .sendForm( 17 | "service_rn5uwdh", 18 | "template_z55djla", 19 | formRef.current, 20 | "onCf_FZuuuG_27Kb_" 21 | ) 22 | .then( 23 | (result) => { 24 | console.log(result.text); 25 | toast.success("Feedback Recorded. We will Contact you shortly"); 26 | }, 27 | (error) => { 28 | console.log(error.text); 29 | toast.error("Something went Wrong , Please try again later"); 30 | } 31 | ); 32 | setLoading(false); 33 | e.target.reset(); // clear input fields 34 | }; 35 | 36 | return ( 37 | <> 38 |
39 |
40 |
41 | {/* Card */} 42 |
43 |

44 | Contact Information 45 |

46 |

47 | Fill the form or contact us via other channels 48 |

49 |
50 |
51 | 57 |
58 | 59 | 91-123-12345 60 |
61 |
62 | 63 | 68 | @kartik_im 69 | 70 |
71 |
72 |
73 |
74 | {/* Form */} 75 |

Contact Us

76 |
81 |
82 | 85 | 92 |
93 |
94 | 97 | 104 |
105 |
106 | 109 | 116 |
117 |
118 | 121 | 127 |
128 | 131 |
132 |
133 |
134 | 135 | ); 136 | }; 137 | 138 | export default Contact; 139 | -------------------------------------------------------------------------------- /src/pages/home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Hero } from "../../components"; 3 | // custom Hook 4 | import useFetchCollection from "../../hooks/useFetchCollection"; 5 | // Redux 6 | import { useDispatch } from "react-redux"; 7 | import { storeProducts, getPriceRange } from "../../redux/slice/productSlice"; 8 | 9 | const Home = () => { 10 | const { data } = useFetchCollection("products"); 11 | const dispatch = useDispatch(); 12 | 13 | useEffect(() => { 14 | dispatch(storeProducts({ products: data })); 15 | dispatch(getPriceRange({ products: data })); 16 | }, [dispatch, data]); 17 | 18 | return ( 19 |
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default Home; 26 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | export { default as Home } from "./home/Home"; 2 | export { default as Admin } from "./admin/Admin"; 3 | export { default as Cart } from "./cart/Cart"; 4 | export { default as OrderHistory } from "./orderHistory/OrderHistory"; 5 | export { default as ResetPassword } from "./resetPassword/ResetPassword"; 6 | export { default as NotFound } from "./notFound/NotFound"; 7 | export { default as AllProducts } from "./allProducts/Allproducts"; 8 | export { default as Contact } from "./contact/Contact"; 9 | export { default as CheckoutDetails } from "./checkout/CheckoutDetails"; 10 | export { default as Checkout } from "./checkout/Checkout"; 11 | export { default as CheckoutSuccess } from "./checkout/CheckoutSuccess"; 12 | export { default as OrderDetails } from "./orderDetails/OrderDetails"; 13 | export { default as Review } from "./reviewProduct/Review"; 14 | -------------------------------------------------------------------------------- /src/pages/notFound/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | const NotFound = () => { 4 | return ( 5 | <> 6 |
7 |
8 |
9 |
10 |
11 |

12 | Looks like you've found the doorway to the great nothing 13 |

14 |

15 | Sorry about that! Please visit our hompage to get where you need 16 | to go. 17 |

18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 | 34 | ); 35 | }; 36 | 37 | export default NotFound; 38 | -------------------------------------------------------------------------------- /src/pages/orderDetails/OrderDetails.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useParams, Link } from "react-router-dom"; 3 | import Loader from "../../components/loader/Loader"; 4 | //firebase 5 | import useFetchDocument from "../../hooks/useFetchDocument"; 6 | 7 | import OrderDetailsComponent from "../../components/orderDetailsComponent/OrderDetailsComponent"; 8 | 9 | const OrderDetails = () => { 10 | const [order, setOrder] = useState(null); 11 | const { id } = useParams(); 12 | const { document } = useFetchDocument("orders", id); 13 | 14 | useEffect(() => { 15 | setOrder(document); 16 | }, [document]); 17 | console.log(order); 18 | return ( 19 | <> 20 | {order === null ? ( 21 | 22 | ) : ( 23 |
24 | 25 |
26 | )} 27 | 28 | ); 29 | }; 30 | 31 | export default OrderDetails; 32 | -------------------------------------------------------------------------------- /src/pages/orderHistory/OrderHistory.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Header, OrdersComponent } from "../../components"; 3 | 4 | import useFetchCollection from "../../hooks/useFetchCollection"; 5 | import Loader from "../../components/loader/Loader"; 6 | import { useNavigate } from "react-router-dom"; 7 | // Redux 8 | import { useDispatch, useSelector } from "react-redux"; 9 | import { storeOrders } from "../../redux/slice/orderSlice"; 10 | import { formatPrice } from "../../utils/formatPrice"; 11 | 12 | const OrderHistory = () => { 13 | const { data, isLoading } = useFetchCollection("orders"); 14 | const { orderHistory } = useSelector((store) => store.order); 15 | const { userId } = useSelector((store) => store.auth); 16 | const dispatch = useDispatch(); 17 | const navigate = useNavigate(); 18 | 19 | useEffect(() => { 20 | dispatch(storeOrders(data)); 21 | }, [dispatch, data]); 22 | 23 | function handleClick(orderId) { 24 | navigate(`/order-details/${orderId}`); 25 | } 26 | 27 | const filteredOrders = orderHistory.filter((order) => order.userId === userId); 28 | 29 | return ( 30 | <> 31 | {isLoading && } 32 |
33 |
34 | 35 |
36 | 37 | ); 38 | }; 39 | 40 | export default OrderHistory; 41 | -------------------------------------------------------------------------------- /src/pages/resetPassword/ResetPassword.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | // FIREBASE 4 | import { sendPasswordResetEmail } from "firebase/auth"; 5 | import { auth } from "../../firebase/config"; 6 | import Loader from "../../components/loader/Loader"; 7 | 8 | const ResetPassword = () => { 9 | const [email, setEmail] = useState(""); 10 | const [isLoading, setIsLoading] = useState(false); 11 | const [err, setErr] = useState(""); 12 | const resetPasswordHandler = (e) => { 13 | e.preventDefault(); 14 | setIsLoading(true); 15 | sendPasswordResetEmail(auth, email) 16 | .then(() => { 17 | toast.info("Check email for reset link"); 18 | setErr("Check your Registered email address for reset link *(Check Spam)*"); 19 | setIsLoading(false); 20 | setEmail(""); 21 | }) 22 | .catch((error) => { 23 | const errorCode = error.code; 24 | const errorMessage = error.message; 25 | setErr(`${errorCode} : ${errorMessage}`); 26 | setIsLoading(false); 27 | }); 28 | }; 29 | 30 | return ( 31 | <> 32 | {isLoading && } 33 | 34 |
35 |
36 |

RESET PASSWORD

37 | {err && ( 38 |

39 | {err} 40 |

41 | )} 42 |
43 | Please enter your registered Email address. You will receive an email 44 | message with instructions on how to reset your password 45 |
46 | 47 |
48 | 49 | setEmail(e.target.value)} 54 | /> 55 | 58 |
59 |
60 |
61 | 62 | ); 63 | }; 64 | 65 | export default ResetPassword; 66 | -------------------------------------------------------------------------------- /src/pages/reviewProduct/Review.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useParams, useNavigate } from "react-router-dom"; 3 | import { Header } from "../../components"; 4 | import { formatPrice } from "../../utils/formatPrice"; 5 | import { toast } from "react-toastify"; 6 | // lazy load 7 | import { LazyLoadImage } from "react-lazy-load-image-component"; 8 | import "react-lazy-load-image-component/src/effects/blur.css"; 9 | // Star rating library 10 | import StarsRating from "react-star-rate"; 11 | //redux 12 | import { useSelector } from "react-redux"; 13 | // firebase 14 | import { collection, addDoc, Timestamp } from "firebase/firestore"; 15 | import { db } from "../../firebase/config"; 16 | 17 | const Review = () => { 18 | const [rating, setRating] = useState(0); 19 | const [review, setReview] = useState(""); 20 | const navigate = useNavigate(); 21 | const { id } = useParams(); 22 | const { products } = useSelector((store) => store.product); 23 | const { userId, userName } = useSelector((store) => store.auth); 24 | 25 | //! find the the matching product from the productsSlice 26 | const filteredProduct = products.find((item) => item.id === id); 27 | 28 | function submitReview(e) { 29 | e.preventDefault(); 30 | const date = new Date().toDateString(); 31 | const time = new Date().toLocaleTimeString(); 32 | const reviewConfig = { 33 | userId, 34 | userName, 35 | productId: id, 36 | review, 37 | rating, 38 | reviewDate: date, 39 | reviewTime: time, 40 | createdAt: Timestamp.now().toDate(), 41 | }; 42 | try { 43 | addDoc(collection(db, "reviews"), reviewConfig); 44 | toast.success("Thanks for Sharing your feedback"); 45 | setRating(0); 46 | setReview(""); 47 | navigate(`/product-details/${id}`); 48 | } catch (error) { 49 | toast.error(error.message); 50 | } 51 | } 52 | 53 | return ( 54 | <> 55 |
56 | {filteredProduct === null ? ( 57 |

No product Found

58 | ) : ( 59 |
60 |
61 |
62 |
63 |

64 | {filteredProduct.name} 65 |

66 |
67 | 74 |
75 |

{filteredProduct.brand}

76 |

77 | {formatPrice(filteredProduct.price)} 78 |

79 |
80 |
81 |
82 |
83 | 84 |
88 |

Rating :

89 | { 92 | setRating(rating); 93 | }} 94 | /> 95 | 102 | 105 | 106 |
107 |
108 | )} 109 | 110 | ); 111 | }; 112 | 113 | export default Review; 114 | -------------------------------------------------------------------------------- /src/redux/slice/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | isUserLoggedIn: false, 5 | email: null, 6 | userName: null, 7 | userId: null, 8 | }; 9 | 10 | const authSlice = createSlice({ 11 | name: "auth", 12 | initialState, 13 | reducers: { 14 | setActiveUser: (state, action) => { 15 | const { email, userName, userId } = action.payload; 16 | state.isUserLoggedIn = true; 17 | state.email = email; 18 | state.userName = userName; 19 | state.userId = userId; 20 | }, 21 | removeActiveUser: (state) => { 22 | state.isUserLoggedIn = false; 23 | state.email = null; 24 | state.userName = null; 25 | state.userId = null; 26 | }, 27 | }, 28 | }); 29 | 30 | export const selectAuthSlice = (store) => store.auth; 31 | 32 | export const { setActiveUser, removeActiveUser } = authSlice.actions; 33 | 34 | export default authSlice.reducer; 35 | -------------------------------------------------------------------------------- /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("cart") ? JSON.parse(localStorage.getItem("cart")) : [], 6 | totalQuantity: 0, 7 | totalAmount: 0, 8 | }; 9 | 10 | const cartSlice = createSlice({ 11 | name: "cart", 12 | initialState, 13 | reducers: { 14 | addToCart: (state, action) => { 15 | // Check if item exists in local Storage Cart 16 | const itemIndex = state.cartItems.findIndex((item) => item.id === action.payload.id); 17 | // item Already exists in the cart 18 | if (itemIndex >= 0) { 19 | state.cartItems[itemIndex].qty += 1; 20 | toast.info(`${action.payload.name} quantity increased `); 21 | } else { 22 | // item does not exist in the cart 23 | const tempProduct = { ...action.payload, qty: 1 }; 24 | state.cartItems.push(tempProduct); 25 | toast.info(`${action.payload.name} Added to cart`); 26 | } 27 | // Add item to local Storaeg 28 | localStorage.setItem("cart", JSON.stringify(state.cartItems)); 29 | }, 30 | decreaseCart: (state, action) => { 31 | const itemIndex = state.cartItems.findIndex((item) => item.id === action.payload.id); 32 | // item has more than 1 qty present 33 | if (state.cartItems[itemIndex].qty > 1) { 34 | state.cartItems[itemIndex].qty -= 1; 35 | toast.error(`${action.payload.name} quantity Decreased `); 36 | // item has only 1 qty 37 | } else if (state.cartItems[itemIndex].qty === 1) { 38 | const newCartItems = state.cartItems.filter((item) => item.id !== action.payload.id); 39 | state.cartItems = newCartItems; 40 | toast.error(`${action.payload.name} removed from Cart`); 41 | } 42 | // new Cart item array after removing items 43 | // Add item to local Storaeg 44 | localStorage.setItem("cart", JSON.stringify(state.cartItems)); 45 | }, 46 | removeCartItem: (state, action) => { 47 | const newCartItems = state.cartItems.filter((item) => item.id !== action.payload.id); 48 | state.cartItems = newCartItems; 49 | localStorage.setItem("cart", JSON.stringify(state.cartItems)); 50 | toast.error(`${action.payload.name} removed from Cart`); 51 | }, 52 | clearCart: (state) => { 53 | state.cartItems = []; 54 | localStorage.setItem("cart", JSON.stringify(state.cartItems)); 55 | toast.info(`All Items removed from Cart`); 56 | }, 57 | calculateSubtotal: (state) => { 58 | const newArr = []; 59 | state.cartItems.map((item) => { 60 | const { price, qty } = item; 61 | const amount = price * qty; 62 | newArr.push(amount); 63 | }); 64 | const totalAmount = newArr.reduce((total, curr) => total + curr, 0); 65 | state.totalAmount = totalAmount; 66 | }, 67 | calculateTotalQuantity: (state) => { 68 | let newArr = []; 69 | state.cartItems.map((item) => { 70 | newArr.push(item.qty); 71 | }); 72 | const totalQty = newArr.reduce((total, curr) => total + curr, 0); 73 | state.totalQuantity = totalQty; 74 | }, 75 | }, 76 | }); 77 | 78 | export const { 79 | addToCart, 80 | decreaseCart, 81 | removeCartItem, 82 | clearCart, 83 | calculateSubtotal, 84 | calculateTotalQuantity, 85 | } = cartSlice.actions; 86 | 87 | export default cartSlice.reducer; 88 | -------------------------------------------------------------------------------- /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 | saveShippingAddress: (state, action) => { 13 | state.shippingAddress = action.payload; 14 | }, 15 | saveBillingAddress: (state, action) => { 16 | state.billingAddress = action.payload; 17 | }, 18 | }, 19 | }); 20 | 21 | export const { saveShippingAddress, saveBillingAddress } = checkoutSlice.actions; 22 | 23 | export default checkoutSlice.reducer; 24 | -------------------------------------------------------------------------------- /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 | filterBySearch(state, action) { 12 | const { products, search } = action.payload; 13 | const tempProducts = products.filter( 14 | (item) => 15 | item.name.toLowerCase().includes(search.toLowerCase()) || 16 | item.category.toLowerCase().includes(search.toLowerCase()) || 17 | item.brand.toLowerCase().includes(search.toLowerCase()) 18 | ); 19 | state.filteredProducts = tempProducts; 20 | }, 21 | sortProducts(state, action) { 22 | const { products, sort } = action.payload; 23 | let tempProducts = []; 24 | let newProductsArray = [...products]; //! do not mutate the products array 25 | if (sort === "latest") { 26 | tempProducts = products; 27 | } 28 | if (sort === "lowest-price") { 29 | tempProducts = newProductsArray.sort((a, b) => a.price - b.price); 30 | } 31 | if (sort === "highest-price") { 32 | tempProducts = newProductsArray.sort((a, b) => b.price - a.price); 33 | } 34 | if (sort === "a2z") { 35 | tempProducts = newProductsArray.sort((a, b) => a.name.localeCompare(b.name)); 36 | } 37 | if (sort === "z2a") { 38 | tempProducts = newProductsArray.sort((a, b) => b.name.localeCompare(a.name)); 39 | } 40 | 41 | state.filteredProducts = tempProducts; 42 | }, 43 | filterByCategory(state, action) { 44 | const { products, category } = action.payload; 45 | let tempProducts = []; 46 | let newProductsArray = [...products]; //! do not mutate the products array 47 | if (category === "All") { 48 | tempProducts = newProductsArray; 49 | } else { 50 | tempProducts = newProductsArray.filter((item) => item.category === category); 51 | } 52 | state.filteredProducts = tempProducts; 53 | }, 54 | filterByBrand(state, action) { 55 | const { products, brand } = action.payload; 56 | let tempProducts = []; 57 | let newProductsArray = [...products]; //! do not mutate the products array 58 | if (brand === "all") { 59 | tempProducts = newProductsArray; 60 | } else { 61 | tempProducts = newProductsArray.filter((item) => item.brand === brand); 62 | } 63 | state.filteredProducts = tempProducts; 64 | }, 65 | filterByprice(state, action) { 66 | const { products, price } = action.payload; 67 | let tempProducts = []; 68 | let newProductsArray = [...products]; 69 | tempProducts = newProductsArray.filter((item) => item.price <= price); 70 | state.filteredProducts = tempProducts; 71 | }, 72 | }, 73 | }); 74 | 75 | export const { filterBySearch, sortProducts, filterByCategory, filterByBrand, filterByprice } = 76 | filterSlice.actions; 77 | 78 | export default filterSlice.reducer; 79 | -------------------------------------------------------------------------------- /src/redux/slice/orderSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | orderHistory: [], 5 | totalAmount: null, 6 | }; 7 | 8 | const orderSlice = createSlice({ 9 | name: "orders", 10 | initialState, 11 | reducers: { 12 | storeOrders: (state, action) => { 13 | state.orderHistory = action.payload; 14 | }, 15 | totalOrderAmount: (state, action) => { 16 | const newArray = []; 17 | state.orderHistory.map((item) => { 18 | const { orderAmount } = item; 19 | newArray.push(orderAmount); 20 | }); 21 | const total = newArray.reduce((total, curr) => total + curr, 0); 22 | state.totalAmount = total; 23 | }, 24 | }, 25 | }); 26 | 27 | export const { storeOrders, totalOrderAmount } = orderSlice.actions; 28 | 29 | export default orderSlice.reducer; 30 | -------------------------------------------------------------------------------- /src/redux/slice/productSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | products: localStorage.getItem("product") ? JSON.parse(localStorage.getItem("product")) : [], 5 | minPrice: 0, 6 | maxPrice: 0, 7 | }; 8 | 9 | const productSlice = createSlice({ 10 | name: "product", 11 | initialState, 12 | reducers: { 13 | storeProducts(state, action) { 14 | state.products = action.payload.products; 15 | localStorage.setItem("product", JSON.stringify(action.payload.products)); 16 | }, 17 | getPriceRange(state, action) { 18 | const { products } = action.payload; 19 | const priceArray = products.map((item) => item.price); 20 | const max = Math.max(...priceArray); 21 | const min = Math.min(...priceArray); 22 | state.minPrice = min; 23 | state.maxPrice = max; 24 | }, 25 | }, 26 | }); 27 | 28 | export const { storeProducts, getPriceRange } = productSlice.actions; 29 | 30 | export default productSlice.reducer; 31 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } 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 | export const store = configureStore({ 10 | reducer: { 11 | auth: authReducer, 12 | product: productReducer, 13 | filter: filterReducer, 14 | cart: cartReducer, 15 | checkout: checkoutReducer, 16 | order: orderReducer, 17 | }, 18 | middleware: (getDefaultMiddleware) => 19 | getDefaultMiddleware({ 20 | serializableCheck: false, 21 | }), 22 | }); 23 | -------------------------------------------------------------------------------- /src/stripeStyles.css: -------------------------------------------------------------------------------- 1 | #root { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | body { 7 | font-family: -apple-system, BlinkMacSystemFont, sans-serif; 8 | font-size: 16px; 9 | -webkit-font-smoothing: antialiased; 10 | display: flex; 11 | justify-content: center; 12 | align-content: center; 13 | height: 100vh; 14 | width: 100vw; 15 | } 16 | 17 | form { 18 | width: 30vw; 19 | min-width: 500px; 20 | align-self: center; 21 | box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1), 0px 2px 5px 0px rgba(50, 50, 93, 0.1), 22 | 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07); 23 | border-radius: 7px; 24 | padding: 40px; 25 | } 26 | 27 | #payment-message { 28 | color: rgb(105, 115, 134); 29 | font-size: 16px; 30 | line-height: 20px; 31 | padding-top: 12px; 32 | text-align: center; 33 | } 34 | 35 | #payment-element { 36 | margin-bottom: 24px; 37 | } 38 | 39 | /* Buttons and links */ 40 | button { 41 | background: #5469d4; 42 | font-family: Arial, sans-serif; 43 | color: #ffffff; 44 | border-radius: 4px; 45 | border: 0; 46 | padding: 12px 16px; 47 | font-size: 16px; 48 | font-weight: 600; 49 | cursor: pointer; 50 | display: block; 51 | transition: all 0.2s ease; 52 | box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07); 53 | width: 100%; 54 | } 55 | 56 | button:hover { 57 | filter: contrast(115%); 58 | } 59 | 60 | button:disabled { 61 | opacity: 0.5; 62 | cursor: default; 63 | } 64 | 65 | /* spinner/processing state, errors */ 66 | .spinner, 67 | .spinner:before, 68 | .spinner:after { 69 | border-radius: 50%; 70 | } 71 | 72 | .spinner { 73 | color: #ffffff; 74 | font-size: 22px; 75 | text-indent: -99999px; 76 | margin: 0px auto; 77 | position: relative; 78 | width: 20px; 79 | height: 20px; 80 | box-shadow: inset 0 0 0 2px; 81 | -webkit-transform: translateZ(0); 82 | -ms-transform: translateZ(0); 83 | transform: translateZ(0); 84 | } 85 | 86 | .spinner:before, 87 | .spinner:after { 88 | position: absolute; 89 | content: ""; 90 | } 91 | 92 | .spinner:before { 93 | width: 10.4px; 94 | height: 20.4px; 95 | background: #5469d4; 96 | border-radius: 20.4px 0 0 20.4px; 97 | top: -0.2px; 98 | left: -0.2px; 99 | -webkit-transform-origin: 10.4px 10.2px; 100 | transform-origin: 10.4px 10.2px; 101 | -webkit-animation: loading 2s infinite ease 1.5s; 102 | animation: loading 2s infinite ease 1.5s; 103 | } 104 | 105 | .spinner:after { 106 | width: 10.4px; 107 | height: 10.2px; 108 | background: #5469d4; 109 | border-radius: 0 10.2px 10.2px 0; 110 | top: -0.1px; 111 | left: 10.2px; 112 | -webkit-transform-origin: 0px 10.2px; 113 | transform-origin: 0px 10.2px; 114 | -webkit-animation: loading 2s infinite ease; 115 | animation: loading 2s infinite ease; 116 | } 117 | 118 | @keyframes loading { 119 | 0% { 120 | -webkit-transform: rotate(0deg); 121 | transform: rotate(0deg); 122 | } 123 | 100% { 124 | -webkit-transform: rotate(360deg); 125 | transform: rotate(360deg); 126 | } 127 | } 128 | 129 | @media only screen and (max-width: 600px) { 130 | form { 131 | width: 80vw; 132 | min-width: initial; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/utils/adminAddProductDefaultValues.js: -------------------------------------------------------------------------------- 1 | export const defaultValues = { 2 | name: "", 3 | imageURL: "", 4 | price: 0, 5 | category: "", 6 | brand: "", 7 | description: "", 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/adminProductCategories.js: -------------------------------------------------------------------------------- 1 | export const categories = [ 2 | { id: 1, name: "Laptop" }, 3 | { id: 2, name: "Electronics" }, 4 | { id: 3, name: "Fashion" }, 5 | { id: 4, name: "Phone" }, 6 | { id: 5, name: "Jewelery" }, 7 | { id: 6, name: "Furniture" }, 8 | ]; 9 | -------------------------------------------------------------------------------- /src/utils/formatPrice.js: -------------------------------------------------------------------------------- 1 | export function formatPrice(price) { 2 | const formatter = Intl.NumberFormat("en-US", { 3 | style: "currency", 4 | currency: "INR", 5 | }).format(price); 6 | return formatter; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/uniqueValues.js: -------------------------------------------------------------------------------- 1 | export const getUniqueValues = (products, type) => { 2 | if (type === "category") { 3 | return ["All", ...new Set(products.map((item) => item.category))]; 4 | } 5 | if (type === "brand") { 6 | return ["all", ...new Set(products.map((item) => item.brand))]; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("daisyui")], 8 | daisyui: { 9 | themes: ["lemonade"], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | --------------------------------------------------------------------------------