├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── backend ├── controllers │ ├── Address.js │ ├── Auth.js │ ├── Brand.js │ ├── Cart.js │ ├── Category.js │ ├── Order.js │ ├── Product.js │ ├── Review.js │ ├── User.js │ └── Wishlist.js ├── database │ └── db.js ├── index.js ├── middleware │ └── VerifyToken.js ├── models │ ├── Address.js │ ├── Brand.js │ ├── Cart.js │ ├── Category.js │ ├── OTP.js │ ├── Order.js │ ├── PasswordResetToken.js │ ├── Product.js │ ├── Review.js │ ├── User.js │ └── Wishlist.js ├── package-lock.json ├── package.json ├── routes │ ├── Address.js │ ├── Auth.js │ ├── Brand.js │ ├── Cart.js │ ├── Category.js │ ├── Order.js │ ├── Product.js │ ├── Review.js │ ├── User.js │ └── Wishlist.js ├── seed │ ├── Address.js │ ├── Brand.js │ ├── Cart.js │ ├── Category.js │ ├── Order.js │ ├── Product.js │ ├── Review.js │ ├── User.js │ ├── Wishlist.js │ └── seed.js ├── utils │ ├── Emails.js │ ├── GenerateOtp.js │ ├── GenerateToken.js │ └── SanitizeUser.js └── vercel.json ├── frontend ├── package-lock.json ├── package.json ├── public │ └── index.html └── src │ ├── App.js │ ├── app │ └── store.js │ ├── assets │ ├── animations │ │ ├── ecommerceOutlook.json │ │ ├── emptyWishlist.json │ │ ├── loading.json │ │ ├── noOrders.json │ │ ├── notFoundPage.json │ │ ├── orderSuccess.json │ │ └── shoppingBag.json │ ├── images │ │ ├── QRCode.png │ │ ├── appStore.png │ │ ├── banner1.jpg │ │ ├── banner2.jpg │ │ ├── banner3.jpg │ │ ├── banner4.jpg │ │ ├── facebook.png │ │ ├── front.png │ │ ├── googlePlay.png │ │ ├── instagram.png │ │ ├── linkedin.png │ │ └── twitter.png │ └── index.js │ ├── config │ └── axios.js │ ├── constants │ └── index.js │ ├── features │ ├── address │ │ ├── AddressApi.jsx │ │ ├── AddressSlice.jsx │ │ └── components │ │ │ └── Address.jsx │ ├── admin │ │ └── components │ │ │ ├── AddProduct.jsx │ │ │ ├── AdminDashBoard.jsx │ │ │ ├── AdminOrders.jsx │ │ │ └── ProductUpdate.jsx │ ├── auth │ │ ├── AuthApi.jsx │ │ ├── AuthSlice.jsx │ │ └── components │ │ │ ├── ForgotPassword.jsx │ │ │ ├── Login.jsx │ │ │ ├── Logout.jsx │ │ │ ├── OtpVerfication.jsx │ │ │ ├── Protected.jsx │ │ │ ├── ResetPassword.jsx │ │ │ └── Signup.jsx │ ├── brands │ │ ├── BrandApi.jsx │ │ └── BrandSlice.jsx │ ├── cart │ │ ├── CartApi.jsx │ │ ├── CartSlice.jsx │ │ └── components │ │ │ ├── Cart.jsx │ │ │ └── CartItem.jsx │ ├── categories │ │ ├── CategoriesApi.jsx │ │ └── CategoriesSlice.jsx │ ├── checkout │ │ └── components │ │ │ └── Checkout.jsx │ ├── footer │ │ └── Footer.jsx │ ├── navigation │ │ └── components │ │ │ └── Navbar.jsx │ ├── order │ │ ├── OrderApi.jsx │ │ ├── OrderSlice.jsx │ │ └── components │ │ │ └── UserOrders.jsx │ ├── products │ │ ├── ProductApi.jsx │ │ ├── ProductSlice.jsx │ │ └── components │ │ │ ├── ProductBanner.jsx │ │ │ ├── ProductCard.jsx │ │ │ ├── ProductDetails.jsx │ │ │ └── ProductList.jsx │ ├── review │ │ ├── ReviewApi.jsx │ │ ├── ReviewSlice.jsx │ │ └── components │ │ │ ├── ReviewItem.jsx │ │ │ └── Reviews.jsx │ ├── user │ │ ├── UserApi.jsx │ │ ├── UserSlice.jsx │ │ └── components │ │ │ └── UserProfile.jsx │ └── wishlist │ │ ├── WishlistApi.jsx │ │ ├── WishlistSlice.jsx │ │ └── components │ │ └── Wishlist.jsx │ ├── hooks │ └── useAuth │ │ ├── useAuthCheck.js │ │ └── useFetchLoggedInUserDetails.js │ ├── index.js │ ├── layout │ └── RootLayout.js │ ├── pages │ ├── AddProductPage.jsx │ ├── AdminDashboardPage.jsx │ ├── AdminOrdersPage.jsx │ ├── CartPage.jsx │ ├── CheckoutPage.jsx │ ├── ForgotPasswordPage.jsx │ ├── HomePage.jsx │ ├── LoginPage.jsx │ ├── NotFoundPage.jsx │ ├── OrderSuccessPage.jsx │ ├── OtpVerificationPage.jsx │ ├── ProductDetailsPage.jsx │ ├── ProductUpdatePage.jsx │ ├── ResetPasswordPage.jsx │ ├── SignupPage.jsx │ ├── UserOrdersPage.jsx │ ├── UserProfilePage.jsx │ ├── WishlistPage.jsx │ └── index.js │ └── theme │ └── theme.js ├── package-lock.json └── readme.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | rishibakshiofficial@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rishibakshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/controllers/Address.js: -------------------------------------------------------------------------------- 1 | const Address = require("../models/Address") 2 | 3 | exports.create=async(req,res)=>{ 4 | try { 5 | const created=new Address(req.body) 6 | await created.save() 7 | res.status(201).json(created) 8 | } catch (error) { 9 | console.log(error); 10 | return res.status(500).json({message:'Error adding address, please trying again later'}) 11 | } 12 | } 13 | 14 | exports.getByUserId = async (req, res) => { 15 | try { 16 | const {id}=req.params 17 | const results=await Address.find({user:id}) 18 | res.status(200).json(results) 19 | 20 | } catch (error) { 21 | console.log(error); 22 | res.status(500).json({message:'Error fetching addresses, please try again later'}) 23 | } 24 | }; 25 | 26 | exports.updateById=async(req,res)=>{ 27 | try { 28 | const {id}=req.params 29 | const updated=await Address.findByIdAndUpdate(id,req.body,{new:true}) 30 | console.log(updated); 31 | res.status(200).json(updated) 32 | } catch (error) { 33 | console.log(error); 34 | res.status(500).json({message:'Error updating address, please try again later'}) 35 | } 36 | } 37 | 38 | exports.deleteById=async(req,res)=>{ 39 | try { 40 | const {id}=req.params 41 | const deleted=await Address.findByIdAndDelete(id) 42 | res.status(200).json(deleted) 43 | } catch (error) { 44 | console.log(error); 45 | res.status(500).json({message:'Error deleting address, please try again later'}) 46 | } 47 | } 48 | 49 | 50 | -------------------------------------------------------------------------------- /backend/controllers/Brand.js: -------------------------------------------------------------------------------- 1 | const Brand=require("../models/Brand") 2 | 3 | exports.getAll=async(req,res)=>{ 4 | try { 5 | const result=await Brand.find({}) 6 | res.status(200).json(result) 7 | } catch (error) { 8 | console.log(error); 9 | res.status(500).json({message:"Error fetching brands"}) 10 | } 11 | } -------------------------------------------------------------------------------- /backend/controllers/Cart.js: -------------------------------------------------------------------------------- 1 | const Cart=require('../models/Cart') 2 | 3 | exports.create=async(req,res)=>{ 4 | try { 5 | const created=await new Cart(req.body).populate({path:"product",populate:{path:"brand"}}); 6 | await created.save() 7 | res.status(201).json(created) 8 | } catch (error) { 9 | console.log(error); 10 | return res.status(500).json({message:'Error adding product to cart, please trying again later'}) 11 | } 12 | } 13 | 14 | exports.getByUserId=async(req,res)=>{ 15 | try { 16 | const {id}=req.params 17 | const result = await Cart.find({ user: id }).populate({path:"product",populate:{path:"brand"}}); 18 | 19 | res.status(200).json(result) 20 | } catch (error) { 21 | console.log(error); 22 | return res.status(500).json({message:'Error fetching cart items, please trying again later'}) 23 | } 24 | } 25 | 26 | exports.updateById=async(req,res)=>{ 27 | try { 28 | const {id}=req.params 29 | const updated=await Cart.findByIdAndUpdate(id,req.body,{new:true}).populate({path:"product",populate:{path:"brand"}}); 30 | res.status(200).json(updated) 31 | } catch (error) { 32 | console.log(error); 33 | return res.status(500).json({message:'Error updating cart items, please trying again later'}) 34 | } 35 | } 36 | 37 | exports.deleteById=async(req,res)=>{ 38 | try { 39 | const {id}=req.params 40 | const deleted=await Cart.findByIdAndDelete(id) 41 | res.status(200).json(deleted) 42 | } catch (error) { 43 | console.log(error); 44 | return res.status(500).json({message:'Error deleting cart item, please trying again later'}) 45 | } 46 | } 47 | 48 | exports.deleteByUserId=async(req,res)=>{ 49 | 50 | try { 51 | const {id}=req.params 52 | await Cart.deleteMany({user:id}) 53 | res.sendStatus(204) 54 | } catch (error) { 55 | console.log(error); 56 | res.status(500).json({message:"Some Error occured while resetting your cart"}) 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /backend/controllers/Category.js: -------------------------------------------------------------------------------- 1 | const Category=require("../models/Category") 2 | 3 | exports.getAll=async(req,res)=>{ 4 | try { 5 | const result=await Category.find({}) 6 | res.status(200).json(result) 7 | } catch (error) { 8 | console.log(error); 9 | res.status(500).json({message:"Error fetching categories"}) 10 | } 11 | } -------------------------------------------------------------------------------- /backend/controllers/Order.js: -------------------------------------------------------------------------------- 1 | const Order = require("../models/Order"); 2 | 3 | exports.create=async(req,res)=>{ 4 | try { 5 | const created=new Order(req.body) 6 | await created.save() 7 | res.status(201).json(created) 8 | } catch (error) { 9 | console.log(error); 10 | return res.status(500).json({message:'Error creating an order, please trying again later'}) 11 | } 12 | } 13 | 14 | exports.getByUserId=async(req,res)=>{ 15 | try { 16 | const {id}=req.params 17 | const results=await Order.find({user:id}) 18 | res.status(200).json(results) 19 | } catch (error) { 20 | console.log(error); 21 | return res.status(500).json({message:'Error fetching orders, please trying again later'}) 22 | } 23 | } 24 | 25 | exports.getAll = async (req, res) => { 26 | try { 27 | let skip=0 28 | let limit=0 29 | 30 | if(req.query.page && req.query.limit){ 31 | const pageSize=req.query.limit 32 | const page=req.query.page 33 | skip=pageSize*(page-1) 34 | limit=pageSize 35 | } 36 | 37 | const totalDocs=await Order.find({}).countDocuments().exec() 38 | const results=await Order.find({}).skip(skip).limit(limit).exec() 39 | 40 | res.header("X-Total-Count",totalDocs) 41 | res.status(200).json(results) 42 | 43 | } catch (error) { 44 | console.log(error); 45 | res.status(500).json({message:'Error fetching orders, please try again later'}) 46 | } 47 | }; 48 | 49 | exports.updateById=async(req,res)=>{ 50 | try { 51 | const {id}=req.params 52 | const updated=await Order.findByIdAndUpdate(id,req.body,{new:true}) 53 | res.status(200).json(updated) 54 | } catch (error) { 55 | console.log(error); 56 | res.status(500).json({message:'Error updating order, please try again later'}) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/controllers/Product.js: -------------------------------------------------------------------------------- 1 | const { Schema, default: mongoose } = require("mongoose") 2 | const Product=require("../models/Product") 3 | 4 | exports.create=async(req,res)=>{ 5 | try { 6 | const created=new Product(req.body) 7 | await created.save() 8 | res.status(201).json(created) 9 | } catch (error) { 10 | console.log(error); 11 | return res.status(500).json({message:'Error adding product, please trying again later'}) 12 | } 13 | } 14 | 15 | exports.getAll = async (req, res) => { 16 | try { 17 | const filter={} 18 | const sort={} 19 | let skip=0 20 | let limit=0 21 | 22 | if(req.query.brand){ 23 | filter.brand={$in:req.query.brand} 24 | } 25 | 26 | if(req.query.category){ 27 | filter.category={$in:req.query.category} 28 | } 29 | 30 | if(req.query.user){ 31 | filter['isDeleted']=false 32 | } 33 | 34 | if(req.query.sort){ 35 | sort[req.query.sort]=req.query.order?req.query.order==='asc'?1:-1:1 36 | } 37 | 38 | if(req.query.page && req.query.limit){ 39 | 40 | const pageSize=req.query.limit 41 | const page=req.query.page 42 | 43 | skip=pageSize*(page-1) 44 | limit=pageSize 45 | } 46 | 47 | const totalDocs=await Product.find(filter).sort(sort).populate("brand").countDocuments().exec() 48 | const results=await Product.find(filter).sort(sort).populate("brand").skip(skip).limit(limit).exec() 49 | 50 | res.set("X-Total-Count",totalDocs) 51 | 52 | res.status(200).json(results) 53 | 54 | } catch (error) { 55 | console.log(error); 56 | res.status(500).json({message:'Error fetching products, please try again later'}) 57 | } 58 | }; 59 | 60 | exports.getById=async(req,res)=>{ 61 | try { 62 | const {id}=req.params 63 | const result=await Product.findById(id).populate("brand").populate("category") 64 | res.status(200).json(result) 65 | } catch (error) { 66 | console.log(error); 67 | res.status(500).json({message:'Error getting product details, please try again later'}) 68 | } 69 | } 70 | 71 | exports.updateById=async(req,res)=>{ 72 | try { 73 | const {id}=req.params 74 | const updated=await Product.findByIdAndUpdate(id,req.body,{new:true}) 75 | res.status(200).json(updated) 76 | } catch (error) { 77 | console.log(error); 78 | res.status(500).json({message:'Error updating product, please try again later'}) 79 | } 80 | } 81 | 82 | exports.undeleteById=async(req,res)=>{ 83 | try { 84 | const {id}=req.params 85 | const unDeleted=await Product.findByIdAndUpdate(id,{isDeleted:false},{new:true}).populate('brand') 86 | res.status(200).json(unDeleted) 87 | } catch (error) { 88 | console.log(error); 89 | res.status(500).json({message:'Error restoring product, please try again later'}) 90 | } 91 | } 92 | 93 | exports.deleteById=async(req,res)=>{ 94 | try { 95 | const {id}=req.params 96 | const deleted=await Product.findByIdAndUpdate(id,{isDeleted:true},{new:true}).populate("brand") 97 | res.status(200).json(deleted) 98 | } catch (error) { 99 | console.log(error); 100 | res.status(500).json({message:'Error deleting product, please try again later'}) 101 | } 102 | } 103 | 104 | 105 | -------------------------------------------------------------------------------- /backend/controllers/Review.js: -------------------------------------------------------------------------------- 1 | const Review=require("../models/Review") 2 | 3 | exports.create=async(req,res)=>{ 4 | try { 5 | console.log(req.body); 6 | const created=await new Review(req.body).populate({path:'user',select:"-password"}) 7 | await created.save() 8 | res.status(201).json(created) 9 | } catch (error) { 10 | console.log(error); 11 | return res.status(500).json({message:'Error posting review, please trying again later'}) 12 | } 13 | } 14 | 15 | exports.getByProductId=async(req,res)=>{ 16 | try { 17 | const {id}=req.params 18 | let skip=0 19 | let limit=0 20 | 21 | if(req.query.page && req.query.limit){ 22 | const pageSize=req.query.limit 23 | const page=req.query.page 24 | 25 | skip=pageSize*(page-1) 26 | limit=pageSize 27 | } 28 | 29 | const totalDocs=await Review.find({product:id}).countDocuments().exec() 30 | const result=await Review.find({product:id}).skip(skip).limit(limit).populate('user').exec() 31 | 32 | res.set("X-total-Count",totalDocs) 33 | res.status(200).json(result) 34 | 35 | } catch (error) { 36 | console.log(error); 37 | res.status(500).json({message:'Error getting reviews for this product, please try again later'}) 38 | } 39 | } 40 | 41 | exports.updateById=async(req,res)=>{ 42 | try { 43 | const {id}=req.params 44 | const updated=await Review.findByIdAndUpdate(id,req.body,{new:true}).populate('user') 45 | res.status(200).json(updated) 46 | } catch (error) { 47 | console.log(error); 48 | res.status(500).json({message:'Error updating review, please try again later'}) 49 | } 50 | } 51 | 52 | exports.deleteById=async(req,res)=>{ 53 | try { 54 | const {id}=req.params 55 | const deleted=await Review.findByIdAndDelete(id) 56 | res.status(200).json(deleted) 57 | } catch (error) { 58 | console.log(error); 59 | res.status(500).json({message:'Error deleting review, please try again later'}) 60 | } 61 | } -------------------------------------------------------------------------------- /backend/controllers/User.js: -------------------------------------------------------------------------------- 1 | const User=require("../models/User") 2 | 3 | exports.getById=async(req,res)=>{ 4 | try { 5 | const {id}=req.params 6 | const result=(await User.findById(id)).toObject() 7 | delete result.password 8 | res.status(200).json(result) 9 | 10 | } catch (error) { 11 | console.log(error); 12 | res.status(500).json({message:'Error getting your details, please try again later'}) 13 | } 14 | } 15 | exports.updateById=async(req,res)=>{ 16 | try { 17 | const {id}=req.params 18 | const updated=(await User.findByIdAndUpdate(id,req.body,{new:true})).toObject() 19 | delete updated.password 20 | res.status(200).json(updated) 21 | 22 | } catch (error) { 23 | console.log(error); 24 | res.status(500).json({message:'Error getting your details, please try again later'}) 25 | } 26 | } -------------------------------------------------------------------------------- /backend/controllers/Wishlist.js: -------------------------------------------------------------------------------- 1 | const Wishlist = require("../models/Wishlist") 2 | 3 | exports.create=async(req,res)=>{ 4 | try { 5 | const created=await new Wishlist(req.body).populate({path:"product",populate:["brand"]}) 6 | await created.save() 7 | res.status(201).json(created) 8 | } catch (error) { 9 | console.log(error); 10 | res.status(500).json({message:"Error adding product to wishlist, please try again later"}) 11 | } 12 | } 13 | exports.getByUserId=async(req,res)=>{ 14 | try { 15 | const {id}=req.params 16 | let skip=0 17 | let limit=0 18 | 19 | if(req.query.page && req.query.limit){ 20 | const pageSize=req.query.limit 21 | const page=req.query.page 22 | 23 | skip=pageSize*(page-1) 24 | limit=pageSize 25 | } 26 | 27 | const result=await Wishlist.find({user:id}).skip(skip).limit(limit).populate({path:"product",populate:['brand']}) 28 | const totalResults=await Wishlist.find({user:id}).countDocuments().exec() 29 | 30 | res.set("X-Total-Count",totalResults) 31 | res.status(200).json(result) 32 | } catch (error) { 33 | console.log(error); 34 | res.status(500).json({message:"Error fetching your wishlist, please try again later"}) 35 | } 36 | } 37 | exports.updateById=async(req,res)=>{ 38 | try { 39 | const {id}=req.params 40 | const updated=await Wishlist.findByIdAndUpdate(id,req.body,{new:true}).populate("product") 41 | res.status(200).json(updated) 42 | } catch (error) { 43 | console.log(error); 44 | res.status(500).json({message:"Error updating your wishlist, please try again later"}) 45 | } 46 | } 47 | exports.deleteById=async(req,res)=>{ 48 | try { 49 | const {id}=req.params 50 | const deleted=await Wishlist.findByIdAndDelete(id) 51 | return res.status(200).json(deleted) 52 | } catch (error) { 53 | console.log(error); 54 | res.status(500).json({message:"Error deleting that product from wishlist, please try again later"}) 55 | } 56 | } -------------------------------------------------------------------------------- /backend/database/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const mongoose=require("mongoose") 3 | 4 | exports.connectToDB=async()=>{ 5 | try { 6 | await mongoose.connect(process.env.MONGO_URI) 7 | console.log('connected to DB'); 8 | } catch (error) { 9 | console.log(error); 10 | } 11 | } -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config() 2 | const express=require('express') 3 | const cors=require('cors') 4 | const morgan=require("morgan") 5 | const cookieParser=require("cookie-parser") 6 | const authRoutes=require("./routes/Auth") 7 | const productRoutes=require("./routes/Product") 8 | const orderRoutes=require("./routes/Order") 9 | const cartRoutes=require("./routes/Cart") 10 | const brandRoutes=require("./routes/Brand") 11 | const categoryRoutes=require("./routes/Category") 12 | const userRoutes=require("./routes/User") 13 | const addressRoutes=require('./routes/Address') 14 | const reviewRoutes=require("./routes/Review") 15 | const wishlistRoutes=require("./routes/Wishlist") 16 | const { connectToDB } = require("./database/db") 17 | 18 | 19 | // server init 20 | const server=express() 21 | 22 | // database connection 23 | connectToDB() 24 | 25 | 26 | // middlewares 27 | server.use(cors({origin:process.env.ORIGIN,credentials:true,exposedHeaders:['X-Total-Count'],methods:['GET','POST','PATCH','DELETE']})) 28 | server.use(express.json()) 29 | server.use(cookieParser()) 30 | server.use(morgan("tiny")) 31 | 32 | // routeMiddleware 33 | server.use("/auth",authRoutes) 34 | server.use("/users",userRoutes) 35 | server.use("/products",productRoutes) 36 | server.use("/orders",orderRoutes) 37 | server.use("/cart",cartRoutes) 38 | server.use("/brands",brandRoutes) 39 | server.use("/categories",categoryRoutes) 40 | server.use("/address",addressRoutes) 41 | server.use("/reviews",reviewRoutes) 42 | server.use("/wishlist",wishlistRoutes) 43 | 44 | 45 | 46 | server.get("/",(req,res)=>{ 47 | res.status(200).json({message:'running'}) 48 | }) 49 | 50 | server.listen(8000,()=>{ 51 | console.log('server [STARTED] ~ http://localhost:8000'); 52 | }) -------------------------------------------------------------------------------- /backend/middleware/VerifyToken.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const jwt=require('jsonwebtoken') 3 | const { sanitizeUser } = require('../utils/SanitizeUser') 4 | 5 | exports.verifyToken=async(req,res,next)=>{ 6 | try { 7 | // extract the token from request cookies 8 | const {token}=req.cookies 9 | 10 | // if token is not there, return 401 response 11 | if(!token){ 12 | return res.status(401).json({message:"Token missing, please login again"}) 13 | } 14 | 15 | // verifies the token 16 | const decodedInfo=jwt.verify(token,process.env.SECRET_KEY) 17 | 18 | // checks if decoded info contains legit details, then set that info in req.user and calls next 19 | if(decodedInfo && decodedInfo._id && decodedInfo.email){ 20 | req.user=decodedInfo 21 | next() 22 | } 23 | 24 | // if token is invalid then sends the response accordingly 25 | else{ 26 | return res.status(401).json({message:"Invalid Token, please login again"}) 27 | } 28 | 29 | } catch (error) { 30 | 31 | console.log(error); 32 | 33 | if (error instanceof jwt.TokenExpiredError) { 34 | return res.status(401).json({ message: "Token expired, please login again" }); 35 | } 36 | else if (error instanceof jwt.JsonWebTokenError) { 37 | return res.status(401).json({ message: "Invalid Token, please login again" }); 38 | } 39 | else { 40 | return res.status(500).json({ message: "Internal Server Error" }); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /backend/models/Address.js: -------------------------------------------------------------------------------- 1 | const mongoose=require('mongoose') 2 | const {Schema}=mongoose 3 | 4 | const addressSchema = new Schema({ 5 | user:{ 6 | type:Schema.Types.ObjectId, 7 | ref:"User", 8 | required:true 9 | }, 10 | street: { 11 | type: String, 12 | required: true, 13 | }, 14 | city: { 15 | type: String, 16 | required: true, 17 | }, 18 | state: { 19 | type: String, 20 | required: true, 21 | }, 22 | phoneNumber:{ 23 | type:String, 24 | required:true, 25 | }, 26 | postalCode: { 27 | type: String, 28 | required: true, 29 | }, 30 | country: { 31 | type: String, 32 | required: true, 33 | }, 34 | type: { 35 | type: String, 36 | required: true, 37 | }, 38 | }); 39 | 40 | module.exports=mongoose.model('Address',addressSchema) -------------------------------------------------------------------------------- /backend/models/Brand.js: -------------------------------------------------------------------------------- 1 | const mongoose=require("mongoose") 2 | const {Schema}=mongoose 3 | 4 | 5 | const brandSchema=new Schema({ 6 | name:{ 7 | type:String, 8 | required:true 9 | } 10 | }) 11 | 12 | module.exports=mongoose.model("Brand",brandSchema) -------------------------------------------------------------------------------- /backend/models/Cart.js: -------------------------------------------------------------------------------- 1 | const mongoose=require("mongoose") 2 | const {Schema}=mongoose 3 | 4 | const cartSchema=new Schema({ 5 | user:{ 6 | type:Schema.Types.ObjectId, 7 | ref:"User", 8 | required:true, 9 | }, 10 | product:{ 11 | type:Schema.Types.ObjectId, 12 | ref:"Product", 13 | required:true 14 | }, 15 | quantity:{ 16 | type:Number, 17 | default:1, 18 | } 19 | },{versionKey:false}) 20 | 21 | module.exports=mongoose.model("Cart",cartSchema) -------------------------------------------------------------------------------- /backend/models/Category.js: -------------------------------------------------------------------------------- 1 | const mongoose=require("mongoose") 2 | const {Schema}=mongoose 3 | 4 | 5 | const categorySchema=new Schema({ 6 | name:{ 7 | type:String, 8 | required:true 9 | } 10 | }) 11 | 12 | module.exports=mongoose.model("Category",categorySchema) -------------------------------------------------------------------------------- /backend/models/OTP.js: -------------------------------------------------------------------------------- 1 | const mongoose=require("mongoose") 2 | const {Schema}=mongoose 3 | 4 | const otpSchema=new Schema({ 5 | user:{ 6 | type:Schema.Types.ObjectId, 7 | ref:"User", 8 | required:true 9 | }, 10 | otp:{ 11 | type:String, 12 | required:true 13 | }, 14 | expiresAt:{ 15 | type:Date, 16 | required:true 17 | }, 18 | }) 19 | 20 | 21 | 22 | module.exports=mongoose.model("OTP",otpSchema) -------------------------------------------------------------------------------- /backend/models/Order.js: -------------------------------------------------------------------------------- 1 | const mongoose=require("mongoose") 2 | const {Schema}=mongoose 3 | 4 | const orderSchema=new Schema({ 5 | user:{ 6 | type:Schema.Types.ObjectId, 7 | ref:"User", 8 | required:true 9 | }, 10 | item:{ 11 | type:[Schema.Types.Mixed], 12 | required:true 13 | }, 14 | address:{ 15 | type:[Schema.Types.Mixed], 16 | required:true 17 | }, 18 | status:{ 19 | type:String, 20 | enum:['Pending','Dispatched','Out for delivery','Cancelled'], 21 | default:'Pending' 22 | }, 23 | paymentMode:{ 24 | type:String, 25 | enum:['COD','UPI','CARD'], 26 | required:true 27 | }, 28 | total:{ 29 | type:Number, 30 | required:true 31 | }, 32 | createdAt:{ 33 | type:Date, 34 | default:Date.now 35 | }, 36 | },{versionKey:false}) 37 | 38 | module.exports=mongoose.model("Order",orderSchema) -------------------------------------------------------------------------------- /backend/models/PasswordResetToken.js: -------------------------------------------------------------------------------- 1 | const mongoose=require("mongoose") 2 | const {Schema}=mongoose 3 | 4 | const passwordResetTokenSchema=new Schema({ 5 | user:{ 6 | type:Schema.Types.ObjectId, 7 | ref:"User", 8 | required:true 9 | }, 10 | token:{ 11 | type:String, 12 | required:true 13 | }, 14 | expiresAt:{ 15 | type:Date, 16 | required:true 17 | }, 18 | }) 19 | 20 | 21 | 22 | module.exports=mongoose.model("PasswordResetToken",passwordResetTokenSchema) -------------------------------------------------------------------------------- /backend/models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose=require("mongoose") 2 | const {Schema}=mongoose 3 | 4 | const productSchema= new Schema({ 5 | title:{ 6 | type:String, 7 | required:true 8 | }, 9 | description:{ 10 | type:String, 11 | required:true 12 | }, 13 | price:{ 14 | type:Number, 15 | required:true 16 | }, 17 | discountPercentage: { 18 | type: Number, 19 | default: 0, 20 | }, 21 | category:{ 22 | type:Schema.Types.ObjectId, 23 | ref:"Category", 24 | required:true 25 | }, 26 | brand:{ 27 | type:Schema.Types.ObjectId, 28 | ref:"Brand", 29 | required:true 30 | }, 31 | stockQuantity:{ 32 | type:Number, 33 | required:true 34 | }, 35 | thumbnail:{ 36 | type:String, 37 | required:true 38 | }, 39 | images:{ 40 | type:[String], 41 | required:true 42 | }, 43 | isDeleted:{ 44 | type:Boolean, 45 | default:false 46 | } 47 | },{timestamps:true,versionKey:false}) 48 | 49 | module.exports=mongoose.model('Product',productSchema) -------------------------------------------------------------------------------- /backend/models/Review.js: -------------------------------------------------------------------------------- 1 | const mongoose=require('mongoose') 2 | const {Schema}=mongoose 3 | 4 | const reviewSchema=new Schema({ 5 | user:{ 6 | type:Schema.Types.ObjectId, 7 | ref:"User", 8 | required:true 9 | }, 10 | product:{ 11 | type:Schema.Types.ObjectId, 12 | ref:"Product", 13 | required:true 14 | }, 15 | rating:{ 16 | type:Number, 17 | required:true, 18 | min:1, 19 | max:5, 20 | }, 21 | comment:{ 22 | type:String, 23 | required:true 24 | }, 25 | createdAt:{ 26 | type:Date, 27 | default:Date.now 28 | }, 29 | },{versionKey:false}) 30 | 31 | 32 | module.exports=mongoose.model("Review",reviewSchema) -------------------------------------------------------------------------------- /backend/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose=require("mongoose") 2 | const {Schema}=mongoose 3 | 4 | const userSchema=new Schema({ 5 | name:{ 6 | type:String, 7 | required:true 8 | }, 9 | email:{ 10 | type:String, 11 | unique:true, 12 | required:true 13 | }, 14 | password:{ 15 | type:String, 16 | required:true 17 | }, 18 | isVerified:{ 19 | type:Boolean, 20 | default:false 21 | }, 22 | isAdmin:{ 23 | type:Boolean, 24 | default:false 25 | } 26 | }) 27 | 28 | module.exports=mongoose.model("User",userSchema) -------------------------------------------------------------------------------- /backend/models/Wishlist.js: -------------------------------------------------------------------------------- 1 | const mongoose=require('mongoose') 2 | const {Schema}=mongoose 3 | 4 | const wishlistSchema=new Schema({ 5 | user:{ 6 | type:Schema.Types.ObjectId, 7 | ref:"User", 8 | require:true 9 | }, 10 | product:{ 11 | type:Schema.Types.ObjectId, 12 | ref:"Product", 13 | require:true 14 | }, 15 | note:{ 16 | type:String, 17 | } 18 | },{timestamps:true,versionKey:false}) 19 | 20 | module.exports=mongoose.model("Wishlist",wishlistSchema) -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "dev": "nodemon index.js", 9 | "seed":"node seed/seed.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "cookie-parser": "^1.4.6", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.3.1", 20 | "express": "^4.18.2", 21 | "jsonwebtoken": "^9.0.2", 22 | "mongoose": "^8.0.2", 23 | "morgan": "^1.10.0", 24 | "nodemailer": "^6.9.7" 25 | }, 26 | "devDependencies": { 27 | "nodemon": "^3.0.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/routes/Address.js: -------------------------------------------------------------------------------- 1 | const express=require('express') 2 | const addressController=require("../controllers/Address") 3 | const router=express.Router() 4 | 5 | router 6 | .post("/",addressController.create) 7 | .get("/user/:id",addressController.getByUserId) 8 | .patch('/:id',addressController.updateById) 9 | .delete('/:id',addressController.deleteById) 10 | 11 | module.exports=router -------------------------------------------------------------------------------- /backend/routes/Auth.js: -------------------------------------------------------------------------------- 1 | const express=require('express') 2 | const router=express.Router() 3 | const authController=require("../controllers/Auth") 4 | const { verifyToken } = require('../middleware/VerifyToken') 5 | 6 | router 7 | .post("/signup",authController.signup) 8 | .post('/login',authController.login) 9 | .post("/verify-otp",authController.verifyOtp) 10 | .post("/resend-otp",authController.resendOtp) 11 | .post("/forgot-password",authController.forgotPassword) 12 | .post("/reset-password",authController.resetPassword) 13 | .get("/check-auth",verifyToken,authController.checkAuth) 14 | .get('/logout',authController.logout) 15 | 16 | 17 | module.exports=router -------------------------------------------------------------------------------- /backend/routes/Brand.js: -------------------------------------------------------------------------------- 1 | const express=require("express") 2 | const brandController=require("../controllers/Brand") 3 | const router=express.Router() 4 | 5 | router 6 | .get("/",brandController.getAll) 7 | 8 | module.exports=router -------------------------------------------------------------------------------- /backend/routes/Cart.js: -------------------------------------------------------------------------------- 1 | const express=require('express') 2 | const cartController=require('../controllers/Cart') 3 | const router=express.Router() 4 | 5 | router 6 | .post("/",cartController.create) 7 | .get("/user/:id",cartController.getByUserId) 8 | .patch("/:id",cartController.updateById) 9 | .delete("/:id",cartController.deleteById) 10 | .delete("/user/:id",cartController.deleteByUserId) 11 | 12 | module.exports=router -------------------------------------------------------------------------------- /backend/routes/Category.js: -------------------------------------------------------------------------------- 1 | const express=require("express") 2 | const categoryController=require("../controllers/Category") 3 | const router=express.Router() 4 | 5 | router 6 | .get("/",categoryController.getAll) 7 | 8 | 9 | module.exports=router -------------------------------------------------------------------------------- /backend/routes/Order.js: -------------------------------------------------------------------------------- 1 | const express=require('express') 2 | const orderController=require("../controllers/Order") 3 | const router=express.Router() 4 | 5 | 6 | router 7 | .post("/",orderController.create) 8 | .get("/",orderController.getAll) 9 | .get("/user/:id",orderController.getByUserId) 10 | .patch("/:id",orderController.updateById) 11 | 12 | 13 | module.exports=router -------------------------------------------------------------------------------- /backend/routes/Product.js: -------------------------------------------------------------------------------- 1 | const express=require('express') 2 | const productController=require("../controllers/Product") 3 | const router=express.Router() 4 | 5 | router 6 | .post("/",productController.create) 7 | .get("/",productController.getAll) 8 | .get("/:id",productController.getById) 9 | .patch("/:id",productController.updateById) 10 | .patch("/undelete/:id",productController.undeleteById) 11 | .delete("/:id",productController.deleteById) 12 | 13 | module.exports=router -------------------------------------------------------------------------------- /backend/routes/Review.js: -------------------------------------------------------------------------------- 1 | const express=require('express') 2 | const reviewController=require("../controllers/Review") 3 | const router=express.Router() 4 | 5 | 6 | router 7 | .post("/",reviewController.create) 8 | .get('/product/:id',reviewController.getByProductId) 9 | .patch('/:id',reviewController.updateById) 10 | .delete("/:id",reviewController.deleteById) 11 | 12 | module.exports=router -------------------------------------------------------------------------------- /backend/routes/User.js: -------------------------------------------------------------------------------- 1 | const express=require("express") 2 | const userController=require("../controllers/User") 3 | const router=express.Router() 4 | 5 | router 6 | .get("/:id",userController.getById) 7 | .patch("/:id",userController.updateById) 8 | 9 | module.exports=router -------------------------------------------------------------------------------- /backend/routes/Wishlist.js: -------------------------------------------------------------------------------- 1 | const express=require("express") 2 | const wishlistController=require("../controllers/Wishlist") 3 | const router=express.Router() 4 | 5 | 6 | router 7 | .post("/",wishlistController.create) 8 | .get("/user/:id",wishlistController.getByUserId) 9 | .patch("/:id",wishlistController.updateById) 10 | .delete("/:id",wishlistController.deleteById) 11 | 12 | module.exports=router -------------------------------------------------------------------------------- /backend/seed/Address.js: -------------------------------------------------------------------------------- 1 | const Address = require("../models/Address"); 2 | 3 | const addresses = [ 4 | { 5 | _id: "65c26398e1e1a2106ac8fbd5", 6 | user: "65b8e564ea5ce114184ccb96", 7 | street: "main 11th", 8 | city: "Indrapuram", 9 | state: "Uttar Pradesh", 10 | phoneNumber: "9452571272", 11 | postalCode: "201012", 12 | country: "India", 13 | type: "Home", 14 | __v: 0, 15 | }, 16 | { 17 | _id: "65c26412e1e1a2106ac8fbd8", 18 | user: "65b8e564ea5ce114184ccb96", 19 | street: "main 18th", 20 | city: "Noida", 21 | state: "Uttar Pradesh", 22 | phoneNumber: "9846286159", 23 | postalCode: "301273", 24 | country: "India", 25 | type: "Buisness", 26 | __v: 0, 27 | }, 28 | ]; 29 | 30 | exports.seedAddress = async () => { 31 | try { 32 | await Address.insertMany(addresses); 33 | console.log("Address seeded successfully"); 34 | } catch (error) { 35 | console.log(error); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /backend/seed/Brand.js: -------------------------------------------------------------------------------- 1 | const Brand = require("../models/Brand"); 2 | 3 | const brands = [ 4 | { _id: "65a7e20102e12c44f59943da", name: "Apple" }, 5 | { _id: "65a7e20102e12c44f59943db", name: "Samsung" }, 6 | { _id: "65a7e20102e12c44f59943dc", name: "OPPO" }, 7 | { _id: "65a7e20102e12c44f59943dd", name: "Huawei" }, 8 | { _id: "65a7e20102e12c44f59943de", name: "Microsoft Surface" }, 9 | { _id: "65a7e20102e12c44f59943df", name: "Infinix" }, 10 | { _id: "65a7e20102e12c44f59943e0", name: "HP Pavilion" }, 11 | { _id: "65a7e20102e12c44f59943e1", name: "Impression of Acqua Di Gio" }, 12 | { _id: "65a7e20102e12c44f59943e2", name: "Royal_Mirage" }, 13 | { _id: "65a7e20102e12c44f59943e3", name: "Fog Scent Xpressio" }, 14 | { _id: "65a7e20102e12c44f59943e4", name: "Al Munakh" }, 15 | { _id: "65a7e20102e12c44f59943e5", name: "Lord - Al-Rehab" }, 16 | { _id: "65a7e20102e12c44f59943e6", name: "L'Oreal Paris" }, 17 | { _id: "65a7e20102e12c44f59943e7", name: "Hemani Tea" }, 18 | { _id: "65a7e20102e12c44f59943e8", name: "Dermive" }, 19 | { _id: "65a7e20102e12c44f59943e9", name: "ROREC White Rice" }, 20 | { _id: "65a7e20102e12c44f59943ea", name: "Fair & Clear" }, 21 | { _id: "65a7e20102e12c44f59943eb", name: "Saaf & Khaas" }, 22 | { _id: "65a7e20102e12c44f59943ec", name: "Bake Parlor Big" }, 23 | { _id: "65a7e20102e12c44f59943ed", name: "Baking Food Items" }, 24 | { _id: "65a7e20102e12c44f59943ee", name: "fauji" }, 25 | { _id: "65a7e20102e12c44f59943ef", name: "Dry Rose" }, 26 | { _id: "65a7e20102e12c44f59943f0", name: "Boho Decor" }, 27 | { _id: "65a7e20102e12c44f59943f1", name: "Flying Wooden" }, 28 | { _id: "65a7e20102e12c44f59943f2", name: "LED Lights" }, 29 | { _id: "65a7e20102e12c44f59943f3", name: "luxury palace" }, 30 | { _id: "65a7e20102e12c44f59943f4", name: "Golden" }, 31 | { _id: "65a7e20102e12c44f59943f5", name: "Furniture Bed Set" }, 32 | { _id: "65a7e20102e12c44f59943f6", name: "Ratttan Outdoor" }, 33 | { _id: "65a7e20102e12c44f59943f7", name: "Kitchen Shelf" }, 34 | { _id: "65a7e20102e12c44f59943f8", name: "Multi Purpose" }, 35 | { _id: "65a7e20102e12c44f59943f9", name: "AmnaMart" }, 36 | { _id: "65a7e20102e12c44f59943fa", name: "Professional Wear" }, 37 | { _id: "65a7e20102e12c44f59943fb", name: "Soft Cotton" }, 38 | { _id: "65a7e20102e12c44f59943fc", name: "Top Sweater" }, 39 | { _id: "65a7e20102e12c44f59943fd", name: "RED MICKY MOUSE.." }, 40 | { _id: "65a7e20102e12c44f59943fe", name: "Digital Printed" }, 41 | { _id: "65a7e20102e12c44f59943ff", name: "Ghazi Fabric" }, 42 | { _id: "65a7e20102e12c44f5994400", name: "IELGY" }, 43 | { _id: "65a7e20102e12c44f5994401", name: "IELGY fashion" }, 44 | { _id: "65a7e20102e12c44f5994402", name: "Synthetic Leather" }, 45 | { _id: "65a7e20102e12c44f5994403", name: "Sandals Flip Flops" }, 46 | { _id: "65a7e20102e12c44f5994404", name: "Maasai Sandals" }, 47 | { _id: "65a7e20102e12c44f5994405", name: "Arrivals Genuine" }, 48 | { _id: "65a7e20102e12c44f5994406", name: "Vintage Apparel" }, 49 | { _id: "65a7e20102e12c44f5994407", name: "FREE FIRE" }, 50 | { _id: "65a7e20102e12c44f5994408", name: "The Warehouse" }, 51 | { _id: "65a7e20102e12c44f5994409", name: "Sneakers" }, 52 | { _id: "65a7e20102e12c44f599440a", name: "Rubber" }, 53 | { _id: "65a7e20102e12c44f599440b", name: "Naviforce" }, 54 | { _id: "65a7e20102e12c44f599440c", name: "SKMEI 9117" }, 55 | { _id: "65a7e20102e12c44f599440d", name: "Strap Skeleton" }, 56 | { _id: "65a7e20102e12c44f599440e", name: "Stainless" }, 57 | { _id: "65a7e20102e12c44f599440f", name: "Eastern Watches" }, 58 | { _id: "65a7e20102e12c44f5994410", name: "Luxury Digital" }, 59 | { _id: "65a7e20102e12c44f5994411", name: "Watch Pearls" }, 60 | { _id: "65a7e20102e12c44f5994412", name: "Bracelet" }, 61 | { _id: "65a7e20102e12c44f5994413", name: "LouisWill" }, 62 | { _id: "65a7e20102e12c44f5994414", name: "Copenhagen Luxe" }, 63 | { _id: "65a7e20102e12c44f5994415", name: "Steal Frame" }, 64 | { _id: "65a7e20102e12c44f5994416", name: "Darojay" }, 65 | { _id: "65a7e20102e12c44f5994417", name: "Fashion Jewellery" }, 66 | { _id: "65a7e20102e12c44f5994418", name: "Cuff Butterfly" }, 67 | { _id: "65a7e20102e12c44f5994419", name: "Designer Sun Glasses" }, 68 | { _id: "65a7e20102e12c44f599441a", name: "mastar watch" }, 69 | { _id: "65a7e20102e12c44f599441b", name: "Car Aux" }, 70 | { _id: "65a7e20102e12c44f599441c", name: "W1209 DC12V" }, 71 | { _id: "65a7e20102e12c44f599441d", name: "TC Reusable" }, 72 | { _id: "65a7e20102e12c44f599441e", name: "Neon LED Light" }, 73 | { _id: "65a7e20102e12c44f599441f", name: "METRO 70cc Motorcycle - MR70" }, 74 | { _id: "65a7e20102e12c44f5994420", name: "BRAVE BULL" }, 75 | { _id: "65a7e20102e12c44f5994421", name: "shock absorber" }, 76 | { _id: "65a7e20102e12c44f5994422", name: "JIEPOLLY" }, 77 | { _id: "65a7e20102e12c44f5994423", name: "Xiangle" }, 78 | { _id: "65a7e20102e12c44f5994424", name: "lightingbrilliance" }, 79 | { _id: "65a7e20102e12c44f5994425", name: "Ifei Home" }, 80 | { _id: "65a7e20102e12c44f5994426", name: "DADAWU" }, 81 | { _id: "65a7e20102e12c44f5994427", name: "YIOSI" }, 82 | ]; 83 | 84 | exports.seedBrand = async () => { 85 | try { 86 | await Brand.insertMany(brands); 87 | console.log('Brand seeded successfully'); 88 | } catch (error) { 89 | console.log(error); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /backend/seed/Cart.js: -------------------------------------------------------------------------------- 1 | const Cart = require("../models/Cart"); 2 | 3 | const cartItems = [ 4 | { 5 | _id: "65c357fe2f21c40d167c276b", 6 | user: "65b8e564ea5ce114184ccb96", 7 | product: "65a7e45902e12c44f5994453", 8 | quantity: 1, 9 | }, 10 | { 11 | _id: "65c3581d2f21c40d167c278f", 12 | user: "65b8e564ea5ce114184ccb96", 13 | product: "65a7e45902e12c44f599445d", 14 | quantity: 4, 15 | }, 16 | { 17 | _id: "65c3584f2f21c40d167c27f5", 18 | user: "65b8e564ea5ce114184ccb96", 19 | product: "65a7e45902e12c44f59944a1", 20 | quantity: 2, 21 | }, 22 | ]; 23 | 24 | exports.seedCart = async () => { 25 | try { 26 | await Cart.insertMany(cartItems); 27 | console.log("Cart seeded successfully"); 28 | } catch (error) { 29 | console.log(error); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /backend/seed/Category.js: -------------------------------------------------------------------------------- 1 | const Category = require("../models/Category"); 2 | 3 | const categories = [ 4 | { _id: "65a7e24602e12c44f599442c", name: "smartphones" }, 5 | { _id: "65a7e24602e12c44f599442d", name: "laptops" }, 6 | { _id: "65a7e24602e12c44f599442e", name: "fragrances" }, 7 | { _id: "65a7e24602e12c44f599442f", name: "skincare" }, 8 | { _id: "65a7e24602e12c44f5994430", name: "groceries" }, 9 | { _id: "65a7e24602e12c44f5994431", name: "home-decoration" }, 10 | { _id: "65a7e24602e12c44f5994432", name: "furniture" }, 11 | { _id: "65a7e24602e12c44f5994433", name: "tops" }, 12 | { _id: "65a7e24602e12c44f5994434", name: "womens-dresses" }, 13 | { _id: "65a7e24602e12c44f5994435", name: "womens-shoes" }, 14 | { _id: "65a7e24602e12c44f5994436", name: "mens-shirts" }, 15 | { _id: "65a7e24602e12c44f5994437", name: "mens-shoes" }, 16 | { _id: "65a7e24602e12c44f5994438", name: "mens-watches" }, 17 | { _id: "65a7e24602e12c44f5994439", name: "womens-watches" }, 18 | { _id: "65a7e24602e12c44f599443a", name: "womens-bags" }, 19 | { _id: "65a7e24602e12c44f599443b", name: "womens-jewellery" }, 20 | { _id: "65a7e24602e12c44f599443c", name: "sunglasses" }, 21 | { _id: "65a7e24602e12c44f599443d", name: "automotive" }, 22 | { _id: "65a7e24602e12c44f599443e", name: "motorcycle" }, 23 | { _id: "65a7e24602e12c44f599443f", name: "lighting" }, 24 | ]; 25 | 26 | exports.seedCategory = async () => { 27 | try { 28 | await Category.insertMany(categories); 29 | console.log("Category seeded successfully"); 30 | } catch (error) { 31 | console.log(error); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /backend/seed/User.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/User"); 2 | 3 | const users = [ 4 | { 5 | _id: "65b8e564ea5ce114184ccb96", 6 | name: "demo user", 7 | email: "demo@gmail.com", 8 | password:'$2a$10$GH8p5cAsGFEdYsLaSfTQ3e1eUs7KbLmVBltjbX4DDCj2eyO2KW/Ze', 9 | isVerified: true, 10 | isAdmin: false, 11 | __v: 0, 12 | }, 13 | { 14 | _id: "65c2526fdcd9253acfbaa731", 15 | name: "rishibakshi", 16 | email: "demo2@gmail.com", 17 | password: '$2a$10$tosjkprqtomSah0VJNyKi.TIv1JU65pl1i1IJ6wUttjYw.ENF99jG', 18 | isVerified: true, 19 | isAdmin: false, 20 | __v: 0, 21 | }, 22 | ]; 23 | 24 | exports.seedUser = async () => { 25 | try { 26 | await User.insertMany(users); 27 | console.log("User seeded successfully"); 28 | } catch (error) { 29 | console.log(error); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /backend/seed/Wishlist.js: -------------------------------------------------------------------------------- 1 | const Wishlist = require("../models/Wishlist"); 2 | 3 | const wishlistItem = [ 4 | { 5 | _id: "65c2441232078478e340ab60", 6 | user: "65b8e564ea5ce114184ccb96", 7 | product: "65a7e45902e12c44f599444e", 8 | createdAt: "2024-02-07T10:11:46.794Z", 9 | updatedAt: "2024-02-07T10:11:46.794Z", 10 | note: "Can't. Stop. Thinking. About. This. Phone. All the new features are giving me major FOMO. Next paycheck, consider yourself spent! **", 11 | }, 12 | { 13 | _id: "65c2441332078478e340ab64", 14 | user: "65b8e564ea5ce114184ccb96", 15 | product: "65a7e45902e12c44f599444f", 16 | createdAt: "2024-02-07T10:11:46.794Z", 17 | updatedAt: "2024-02-07T10:11:46.794Z", 18 | note: "My signature scent just got an upgrade! This perfume is the perfect addition to my collection. Next paycheck, we meet again!", 19 | }, 20 | { 21 | _id: "65c2441532078478e340ab68", 22 | user: "65b8e564ea5ce114184ccb96", 23 | product: "65a7e45902e12c44f5994450", 24 | createdAt: "2024-02-07T10:11:46.794Z", 25 | updatedAt: "2024-02-07T10:11:46.794Z", 26 | note: "Goodbye, clunky laptop! This lightweight tablet would be my new travel buddy for working remotely, catching up on emails, and staying connected. ✈️", 27 | }, 28 | { 29 | _id: "65c2441732078478e340ab6c", 30 | user: "65b8e564ea5ce114184ccb96", 31 | product: "65a7e45902e12c44f5994456", 32 | createdAt: "2024-02-07T10:11:46.794Z", 33 | updatedAt: "2024-02-07T10:11:46.794Z", 34 | note: "Gaming beast unlocked! This laptop with its latest features like dedicated graphics card, fast refresh rate, powerful cooling system] would be the ultimate gaming machine. Time to conquer those virtual worlds! ⚔️", 35 | }, 36 | { 37 | _id: "65c2441a32078478e340ab70", 38 | user: "65b8e564ea5ce114184ccb96", 39 | product: "65a7e45902e12c44f5994452", 40 | createdAt: "2024-02-07T10:11:46.794Z", 41 | updatedAt: "2024-02-07T10:11:46.794Z", 42 | note: "Have to buy this for my friend's birthday", 43 | }, 44 | { 45 | _id: "65c2442132078478e340ab7a", 46 | user: "65b8e564ea5ce114184ccb96", 47 | product: "65a7e45902e12c44f59944b0", 48 | createdAt: "2024-02-07T10:11:46.794Z", 49 | updatedAt: "2024-02-07T10:11:46.794Z", 50 | note: "Mood magic! These smart lights would transform my living room into the perfect movie night haven with customisable colours and dimming. Goodbye, harsh overhead lights! ✨", 51 | }, 52 | { 53 | _id: "65c2443632078478e340ab9a", 54 | user: "65b8e564ea5ce114184ccb96", 55 | product: "65a7e45902e12c44f5994474", 56 | createdAt: "2024-02-07T10:11:46.794Z", 57 | updatedAt: "2024-02-07T10:11:46.794Z", 58 | note: "A perfect christmas gift for my wife", 59 | }, 60 | { 61 | _id: "65c2444732078478e340aba4", 62 | user: "65b8e564ea5ce114184ccb96", 63 | product: "65a7e45902e12c44f5994481", 64 | createdAt: "2024-02-07T10:11:46.794Z", 65 | updatedAt: "2024-02-07T10:11:46.794Z", 66 | note: "A perfect gift for my relative's kid", 67 | }, 68 | { 69 | _id: "65c2445332078478e340abab", 70 | user: "65b8e564ea5ce114184ccb96", 71 | product: "65a7e45902e12c44f599448a", 72 | createdAt: "2024-02-07T10:11:46.794Z", 73 | updatedAt: "2024-02-07T10:11:46.794Z", 74 | note: "A nice decent watch like this would be a perfect match with my outfit this saturday night", 75 | }, 76 | { 77 | _id: "65c2447032078478e340abd4", 78 | user: "65b8e564ea5ce114184ccb96", 79 | product: "65a7e45902e12c44f59944a2", 80 | createdAt: "2024-02-07T10:11:46.794Z", 81 | updatedAt: "2024-02-07T10:11:46.794Z", 82 | note: "Have to buy this for upcoming beach party", 83 | }, 84 | ]; 85 | 86 | exports.seedWishlist = async () => { 87 | try { 88 | await Wishlist.insertMany(wishlistItem); 89 | console.log("Wishlist seeded successfully"); 90 | } catch (error) { 91 | console.log(error); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /backend/seed/seed.js: -------------------------------------------------------------------------------- 1 | const {seedBrand}=require("./Brand") 2 | const {seedCategory}=require("./Category") 3 | const {seedProduct}=require("./Product") 4 | const {seedUser}=require("./User") 5 | const {seedAddress}=require("./Address") 6 | const {seedWishlist}=require("./Wishlist") 7 | const {seedCart}=require("./Cart") 8 | const {seedReview}=require("./Review") 9 | const {seedOrder}=require("./Order") 10 | const {connectToDB}=require("../database/db") 11 | 12 | const seedData=async()=>{ 13 | try { 14 | await connectToDB() 15 | console.log('Seed [started] please wait..'); 16 | await seedBrand() 17 | await seedCategory() 18 | await seedProduct() 19 | await seedUser() 20 | await seedAddress() 21 | await seedWishlist() 22 | await seedCart() 23 | await seedReview() 24 | await seedOrder() 25 | 26 | console.log('Seed completed..'); 27 | } catch (error) { 28 | console.log(error); 29 | } 30 | } 31 | 32 | seedData() -------------------------------------------------------------------------------- /backend/utils/Emails.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require("nodemailer"); 2 | 3 | const transporter = nodemailer.createTransport({ 4 | service: "gmail", 5 | auth: { 6 | user: process.env.EMAIL, 7 | pass: process.env.PASSWORD, 8 | }, 9 | }); 10 | 11 | exports.sendMail = async(receiverEmail,subject,body) => { 12 | await transporter.sendMail({ 13 | from: process.env.EMAIL, 14 | to: receiverEmail, 15 | subject: subject, 16 | html: body 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /backend/utils/GenerateOtp.js: -------------------------------------------------------------------------------- 1 | exports.generateOTP=()=>{ 2 | const otp = Math.floor(1000 + Math.random() * 9000); 3 | return otp.toString(); 4 | } 5 | 6 | -------------------------------------------------------------------------------- /backend/utils/GenerateToken.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const jwt=require('jsonwebtoken') 3 | 4 | exports.generateToken=(payload,passwordReset=false)=>{ 5 | return jwt.sign(payload,process.env.SECRET_KEY,{expiresIn:passwordReset?process.env.PASSWORD_RESET_TOKEN_EXPIRATION:process.env.LOGIN_TOKEN_EXPIRATION}) 6 | } -------------------------------------------------------------------------------- /backend/utils/SanitizeUser.js: -------------------------------------------------------------------------------- 1 | exports.sanitizeUser=(user)=>{ 2 | return {_id:user._id,email:user.email,isVerified:user.isVerified,isAdmin:user.isAdmin} 3 | } -------------------------------------------------------------------------------- /backend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":2, 3 | "builds":[ 4 | {"src":"*.js","use":"@vercel/node"} 5 | ], 6 | "routes":[ 7 | { 8 | "src":"/(.*)", 9 | "dest":"/" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.11.1", 7 | "@emotion/styled": "^11.11.0", 8 | "@mui/icons-material": "^5.14.19", 9 | "@mui/lab": "^5.0.0-alpha.155", 10 | "@mui/material": "^5.14.20", 11 | "@reduxjs/toolkit": "^2.0.1", 12 | "@testing-library/jest-dom": "^5.17.0", 13 | "@testing-library/react": "^13.4.0", 14 | "@testing-library/user-event": "^13.5.0", 15 | "axios": "^1.6.2", 16 | "framer-motion": "^10.18.0", 17 | "lottie-react": "^2.4.0", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-hook-form": "^7.48.2", 21 | "react-redux": "^9.0.2", 22 | "react-router-dom": "^6.26.1", 23 | "react-scripts": "5.0.1", 24 | "react-swipeable-views": "^0.14.0", 25 | "react-toastify": "^9.1.3", 26 | "web-vitals": "^2.1.4" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 27 | React-Ecommerce 28 | 29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { 3 | Navigate, 4 | Route, RouterProvider, createBrowserRouter, createRoutesFromElements 5 | } from "react-router-dom"; 6 | import { selectIsAuthChecked, selectLoggedInUser } from './features/auth/AuthSlice'; 7 | import { Logout } from './features/auth/components/Logout'; 8 | import { Protected } from './features/auth/components/Protected'; 9 | import { useAuthCheck } from "./hooks/useAuth/useAuthCheck"; 10 | import { useFetchLoggedInUserDetails } from "./hooks/useAuth/useFetchLoggedInUserDetails"; 11 | import { AddProductPage, AdminOrdersPage, CartPage, CheckoutPage, ForgotPasswordPage, HomePage, LoginPage, OrderSuccessPage, OtpVerificationPage, ProductDetailsPage, ProductUpdatePage, ResetPasswordPage, SignupPage, UserOrdersPage, UserProfilePage, WishlistPage } from './pages'; 12 | import { AdminDashboardPage } from './pages/AdminDashboardPage'; 13 | import { NotFoundPage } from './pages/NotFoundPage'; 14 | 15 | 16 | function App() { 17 | 18 | const isAuthChecked=useSelector(selectIsAuthChecked) 19 | const loggedInUser=useSelector(selectLoggedInUser) 20 | 21 | 22 | useAuthCheck(); 23 | useFetchLoggedInUserDetails(loggedInUser); 24 | 25 | 26 | const routes = createBrowserRouter( 27 | createRoutesFromElements( 28 | <> 29 | }/> 30 | }/> 31 | }/> 32 | }/> 33 | }/> 34 | }/> 35 | }/> 36 | 37 | { 38 | loggedInUser?.isAdmin?( 39 | // admin routes 40 | <> 41 | }/> 42 | }/> 43 | }/> 44 | }/> 45 | }/> 46 | 47 | ):( 48 | // user routes 49 | <> 50 | }/> 51 | }/> 52 | }/> 53 | }/> 54 | }/> 55 | }/> 56 | }/> 57 | 58 | ) 59 | } 60 | 61 | } /> 62 | 63 | 64 | ) 65 | ) 66 | 67 | 68 | return isAuthChecked ? : ""; 69 | } 70 | 71 | export default App; 72 | -------------------------------------------------------------------------------- /frontend/src/app/store.js: -------------------------------------------------------------------------------- 1 | import {configureStore} from '@reduxjs/toolkit' 2 | import AuthSlice from '../features/auth/AuthSlice' 3 | import ProductSlice from '../features/products/ProductSlice' 4 | import UserSlice from '../features/user/UserSlice' 5 | import BrandSlice from '../features/brands/BrandSlice' 6 | import CategoriesSlice from '../features/categories/CategoriesSlice' 7 | import CartSlice from '../features/cart/CartSlice' 8 | import AddressSlice from '../features/address/AddressSlice' 9 | import ReviewSlice from '../features/review/ReviewSlice' 10 | import OrderSlice from '../features/order/OrderSlice' 11 | import WishlistSlice from '../features/wishlist/WishlistSlice' 12 | 13 | export const store=configureStore({ 14 | reducer:{ 15 | AuthSlice, 16 | ProductSlice, 17 | UserSlice, 18 | BrandSlice, 19 | CategoriesSlice, 20 | CartSlice, 21 | AddressSlice, 22 | ReviewSlice, 23 | OrderSlice, 24 | WishlistSlice 25 | } 26 | }) -------------------------------------------------------------------------------- /frontend/src/assets/images/QRCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/QRCode.png -------------------------------------------------------------------------------- /frontend/src/assets/images/appStore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/appStore.png -------------------------------------------------------------------------------- /frontend/src/assets/images/banner1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/banner1.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/banner2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/banner2.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/banner3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/banner3.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/banner4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/banner4.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/facebook.png -------------------------------------------------------------------------------- /frontend/src/assets/images/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/front.png -------------------------------------------------------------------------------- /frontend/src/assets/images/googlePlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/googlePlay.png -------------------------------------------------------------------------------- /frontend/src/assets/images/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/instagram.png -------------------------------------------------------------------------------- /frontend/src/assets/images/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/linkedin.png -------------------------------------------------------------------------------- /frontend/src/assets/images/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishiBakshii/mern-ecommerce/82303dc20b9d9ccfd0b45ecb3a6217eafe481491/frontend/src/assets/images/twitter.png -------------------------------------------------------------------------------- /frontend/src/assets/index.js: -------------------------------------------------------------------------------- 1 | import ecommerceOutlookAnimation from './animations/ecommerceOutlook.json' 2 | import shoppingBagAnimation from './animations/shoppingBag.json' 3 | import orderSuccessAnimation from './animations/orderSuccess.json' 4 | import noOrdersAnimation from './animations/noOrders.json' 5 | import emptyWishlistAnimation from './animations/emptyWishlist.json' 6 | import notFoundPageAnimation from './animations/notFoundPage.json' 7 | import banner1 from './images/banner1.jpg' 8 | import googlePlayPng from './images/googlePlay.png' 9 | import appStorePng from './images/appStore.png' 10 | import QRCodePng from './images/QRCode.png' 11 | import facebookPng from './images/facebook.png' 12 | import twitterPng from './images/twitter.png' 13 | import instagramPng from './images/instagram.png' 14 | import linkedinPng from './images/linkedin.png' 15 | import banner2 from './images/banner2.jpg' 16 | import banner3 from './images/banner3.jpg' 17 | import banner4 from './images/banner4.jpg' 18 | import loadingAnimation from './animations/loading.json' 19 | 20 | export { 21 | ecommerceOutlookAnimation, 22 | shoppingBagAnimation, 23 | orderSuccessAnimation, 24 | noOrdersAnimation, 25 | emptyWishlistAnimation, 26 | notFoundPageAnimation, 27 | banner1, 28 | googlePlayPng, 29 | appStorePng, 30 | QRCodePng, 31 | facebookPng, 32 | twitterPng, 33 | instagramPng, 34 | linkedinPng, 35 | banner2, 36 | banner3, 37 | banner4, 38 | loadingAnimation 39 | } -------------------------------------------------------------------------------- /frontend/src/config/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const axiosi=axios.create({withCredentials:true,baseURL:process.env.REACT_APP_BASE_URL}) -------------------------------------------------------------------------------- /frontend/src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const SHIPPING=5.55 2 | export const TAXES=5 3 | export const ITEMS_PER_PAGE=10 -------------------------------------------------------------------------------- /frontend/src/features/address/AddressApi.jsx: -------------------------------------------------------------------------------- 1 | import { axiosi } from "../../config/axios"; 2 | 3 | export const addAddress=async(address)=>{ 4 | try { 5 | const res=await axiosi.post("/address",address) 6 | return res.data 7 | } catch (error) { 8 | throw error.response.data 9 | } 10 | } 11 | export const fetchAddressByUserId=async(id)=>{ 12 | try { 13 | const res=await axiosi.get(`/address/user/${id}`) 14 | return res.data 15 | } catch (error) { 16 | throw error.response.data 17 | } 18 | } 19 | export const updateAddressById=async(update)=>{ 20 | try { 21 | const res=await axiosi.patch(`/address/${update._id}`,update) 22 | return res.data 23 | } catch (error) { 24 | throw error.response.data 25 | } 26 | } 27 | export const deleteAddressById=async(id)=>{ 28 | try { 29 | const res=await axiosi.delete(`/address/${id}`) 30 | return res.data 31 | } catch (error) { 32 | throw error.response.data 33 | } 34 | } -------------------------------------------------------------------------------- /frontend/src/features/address/AddressSlice.jsx: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 2 | import { addAddress, deleteAddressById, fetchAddressByUserId, updateAddressById } from "./AddressApi"; 3 | 4 | 5 | const initialState={ 6 | status:"idle", 7 | addressAddStatus:"idle", 8 | addressDeleteStatus:"idle", 9 | addressUpdateStatus:"idle", 10 | addresses:[], 11 | errors:null, 12 | successMessage:null 13 | } 14 | 15 | 16 | export const addAddressAsync=createAsyncThunk('address/addAddressAsync',async(address)=>{ 17 | const createdAddress=await addAddress(address) 18 | return createdAddress 19 | }) 20 | export const fetchAddressByUserIdAsync=createAsyncThunk('address/fetchAddressByUserIdAsync',async(id)=>{ 21 | const addresses=await fetchAddressByUserId(id) 22 | return addresses 23 | }) 24 | export const updateAddressByIdAsync=createAsyncThunk('address/updateAddressByIdAsync',async(id)=>{ 25 | const updatedAddress=await updateAddressById(id) 26 | return updatedAddress 27 | }) 28 | export const deleteAddressByIdAsync=createAsyncThunk('address/deleteAddressByIdAsync',async(id)=>{ 29 | const deletedAddress=await deleteAddressById(id) 30 | return deletedAddress 31 | }) 32 | 33 | const addressSlice=createSlice({ 34 | name:"addressSlice", 35 | initialState:initialState, 36 | reducers:{ 37 | resetAddressStatus:(state)=>{ 38 | state.status='idle' 39 | }, 40 | resetAddressAddStatus:(state)=>{ 41 | state.addressAddStatus="idle" 42 | }, 43 | resetAddressDeleteStatus:(state)=>{ 44 | state.addressDeleteStatus="idle" 45 | }, 46 | resetAddressUpdateStatus:(state)=>{ 47 | state.addressUpdateStatus="idle" 48 | }, 49 | }, 50 | extraReducers:(builder)=>{ 51 | builder 52 | .addCase(addAddressAsync.pending,(state)=>{ 53 | state.addressAddStatus='pending' 54 | }) 55 | .addCase(addAddressAsync.fulfilled,(state,action)=>{ 56 | state.addressAddStatus='fulfilled' 57 | state.addresses.push(action.payload) 58 | }) 59 | .addCase(addAddressAsync.rejected,(state,action)=>{ 60 | state.addressAddStatus='rejected' 61 | state.errors=action.error 62 | }) 63 | 64 | .addCase(fetchAddressByUserIdAsync.pending,(state)=>{ 65 | state.status='pending' 66 | }) 67 | .addCase(fetchAddressByUserIdAsync.fulfilled,(state,action)=>{ 68 | state.status='fulfilled' 69 | state.addresses=action.payload 70 | }) 71 | .addCase(fetchAddressByUserIdAsync.rejected,(state,action)=>{ 72 | state.status='rejected' 73 | state.errors=action.error 74 | }) 75 | 76 | .addCase(updateAddressByIdAsync.pending,(state)=>{ 77 | state.addressUpdateStatus='pending' 78 | }) 79 | .addCase(updateAddressByIdAsync.fulfilled,(state,action)=>{ 80 | state.addressUpdateStatus='fulfilled' 81 | const index=state.addresses.findIndex((address)=>address._id===action.payload._id) 82 | state.addresses[index]=action.payload 83 | }) 84 | .addCase(updateAddressByIdAsync.rejected,(state,action)=>{ 85 | state.addressUpdateStatus='rejected' 86 | state.errors=action.error 87 | }) 88 | 89 | .addCase(deleteAddressByIdAsync.pending,(state)=>{ 90 | state.addressDeleteStatus='pending' 91 | }) 92 | .addCase(deleteAddressByIdAsync.fulfilled,(state,action)=>{ 93 | state.addressDeleteStatus='fulfilled' 94 | state.addresses=state.addresses.filter((address)=>address._id!==action.payload._id) 95 | }) 96 | .addCase(deleteAddressByIdAsync.rejected,(state,action)=>{ 97 | state.addressDeleteStatus='rejected' 98 | state.errors=action.error 99 | }) 100 | } 101 | }) 102 | 103 | // exporting selectors 104 | export const selectAddressStatus=(state)=>state.AddressSlice.status 105 | export const selectAddresses=(state)=>state.AddressSlice.addresses 106 | export const selectAddressErrors=(state)=>state.AddressSlice.errors 107 | export const selectAddressSuccessMessage=(state)=>state.AddressSlice.successMessage 108 | export const selectAddressAddStatus=(state)=>state.AddressSlice.addressAddStatus 109 | export const selectAddressDeleteStatus=(state)=>state.AddressSlice.addressDeleteStatus 110 | export const selectAddressUpdateStatus=(state)=>state.AddressSlice.addressUpdateStatus 111 | 112 | // exporting reducers 113 | export const {resetAddressStatus,resetAddressAddStatus,resetAddressDeleteStatus,resetAddressUpdateStatus}=addressSlice.actions 114 | 115 | export default addressSlice.reducer -------------------------------------------------------------------------------- /frontend/src/features/address/components/Address.jsx: -------------------------------------------------------------------------------- 1 | import { LoadingButton } from '@mui/lab' 2 | import { Button, Paper, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material' 3 | import React, { useState } from 'react' 4 | import { useForm } from "react-hook-form" 5 | import { useDispatch, useSelector } from 'react-redux' 6 | import { deleteAddressByIdAsync, selectAddressErrors, selectAddressStatus, updateAddressByIdAsync } from '../AddressSlice' 7 | 8 | export const Address = ({id,type,street,postalCode,country,phoneNumber,state,city}) => { 9 | 10 | const theme=useTheme() 11 | const dispatch=useDispatch() 12 | const {register,handleSubmit,watch,reset,formState: { errors }} = useForm() 13 | const [edit,setEdit]=useState(false) 14 | const [open, setOpen] = useState(false); 15 | const status=useSelector(selectAddressStatus) 16 | const error=useSelector(selectAddressErrors) 17 | 18 | const is480=useMediaQuery(theme.breakpoints.down(480)) 19 | 20 | const handleRemoveAddress=()=>{ 21 | dispatch(deleteAddressByIdAsync(id)) 22 | } 23 | 24 | const handleUpdateAddress=(data)=>{ 25 | const update={...data,_id:id} 26 | setEdit(false) 27 | dispatch(updateAddressByIdAsync(update)) 28 | } 29 | 30 | 31 | return ( 32 | 33 | 34 | {/* address type */} 35 | 36 | {type?.toUpperCase()} 37 | 38 | 39 | {/* address details */} 40 | 41 | 42 | {/* if the edit is true then this update from shows*/} 43 | { 44 | edit? 45 | ( 46 | // update address form 47 | 48 | 49 | 50 | Type 51 | 52 | 53 | 54 | 55 | 56 | Street 57 | 58 | 59 | 60 | 61 | Postal Code 62 | 63 | 64 | 65 | 66 | Country 67 | 68 | 69 | 70 | 71 | Phone Number 72 | 73 | 74 | 75 | 76 | State 77 | 78 | 79 | 80 | 81 | City 82 | 83 | 84 | 85 | ):( 86 | <> 87 | Street - {street} 88 | Postal Code- {postalCode} 89 | Country - {country} 90 | Phone Number - {phoneNumber} 91 | State - {state} 92 | City - {city} 93 | 94 | ) 95 | } 96 | 97 | {/* action buttons */} 98 | 99 | 100 | {/* if edit is true, then save changes button is shown instead of edit*/} 101 | { 102 | edit?(Save Changes 103 | ):() 104 | } 105 | 106 | {/* if edit is true then cancel button is shown instead of remove */} 107 | { 108 | edit?( 109 | 110 | ):( 111 | Remove 112 | ) 113 | } 114 | 115 | 116 | 117 | 118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /frontend/src/features/admin/components/AddProduct.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { Link, useNavigate} from 'react-router-dom' 4 | import { addProductAsync, resetProductAddStatus, selectProductAddStatus,updateProductByIdAsync } from '../../products/ProductSlice' 5 | import { Button, FormControl, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material' 6 | import { useForm } from "react-hook-form" 7 | import { selectBrands } from '../../brands/BrandSlice' 8 | import { selectCategories } from '../../categories/CategoriesSlice' 9 | import { toast } from 'react-toastify' 10 | 11 | export const AddProduct = () => { 12 | 13 | const {register,handleSubmit,reset,formState: { errors }} = useForm() 14 | 15 | const dispatch=useDispatch() 16 | const brands=useSelector(selectBrands) 17 | const categories=useSelector(selectCategories) 18 | const productAddStatus=useSelector(selectProductAddStatus) 19 | const navigate=useNavigate() 20 | const theme=useTheme() 21 | const is1100=useMediaQuery(theme.breakpoints.down(1100)) 22 | const is480=useMediaQuery(theme.breakpoints.down(480)) 23 | 24 | useEffect(()=>{ 25 | if(productAddStatus==='fullfilled'){ 26 | reset() 27 | toast.success("New product added") 28 | navigate("/admin/dashboard") 29 | } 30 | else if(productAddStatus==='rejected'){ 31 | toast.error("Error adding product, please try again later") 32 | } 33 | },[productAddStatus]) 34 | 35 | useEffect(()=>{ 36 | return ()=>{ 37 | dispatch(resetProductAddStatus()) 38 | } 39 | },[]) 40 | 41 | const handleAddProduct=(data)=>{ 42 | const newProduct={...data,images:[data.image0,data.image1,data.image2,data.image3]} 43 | delete newProduct.image0 44 | delete newProduct.image1 45 | delete newProduct.image2 46 | delete newProduct.image3 47 | 48 | dispatch(addProductAsync(newProduct)) 49 | } 50 | 51 | 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | {/* feild area */} 59 | 60 | 61 | Title 62 | 63 | 64 | 65 | 66 | 67 | 68 | Brand 69 | 78 | 79 | 80 | 81 | 82 | Category 83 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Description 99 | 100 | 101 | 102 | 103 | 104 | Price 105 | 106 | 107 | 108 | Discount {is480?"%":"Percentage"} 109 | 110 | 111 | 112 | 113 | 114 | Stock Quantity 115 | 116 | 117 | 118 | Thumbnail 119 | 120 | 121 | 122 | 123 | Product Images 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | {/* action area */} 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | ) 148 | } 149 | -------------------------------------------------------------------------------- /frontend/src/features/admin/components/ProductUpdate.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { Link, useNavigate, useParams } from 'react-router-dom' 4 | import { clearSelectedProduct, fetchProductByIdAsync,resetProductUpdateStatus, selectProductUpdateStatus, selectSelectedProduct, updateProductByIdAsync } from '../../products/ProductSlice' 5 | import { Button, FormControl, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material' 6 | import { useForm } from "react-hook-form" 7 | import { selectBrands } from '../../brands/BrandSlice' 8 | import { selectCategories } from '../../categories/CategoriesSlice' 9 | import { toast } from 'react-toastify' 10 | 11 | export const ProductUpdate = () => { 12 | 13 | const {register,handleSubmit,watch,formState: { errors }} = useForm() 14 | 15 | const {id}=useParams() 16 | const dispatch=useDispatch() 17 | const selectedProduct=useSelector(selectSelectedProduct) 18 | const brands=useSelector(selectBrands) 19 | const categories=useSelector(selectCategories) 20 | const productUpdateStatus=useSelector(selectProductUpdateStatus) 21 | const navigate=useNavigate() 22 | const theme=useTheme() 23 | const is1100=useMediaQuery(theme.breakpoints.down(1100)) 24 | const is480=useMediaQuery(theme.breakpoints.down(480)) 25 | 26 | 27 | useEffect(()=>{ 28 | if(id){ 29 | dispatch(fetchProductByIdAsync(id)) 30 | } 31 | },[id]) 32 | 33 | useEffect(()=>{ 34 | if(productUpdateStatus==='fullfilled'){ 35 | toast.success("Product Updated") 36 | navigate("/admin/dashboard") 37 | } 38 | else if(productUpdateStatus==='rejected'){ 39 | toast.error("Error updating product, please try again later") 40 | } 41 | },[productUpdateStatus]) 42 | 43 | useEffect(()=>{ 44 | return ()=>{ 45 | dispatch(clearSelectedProduct()) 46 | dispatch(resetProductUpdateStatus()) 47 | } 48 | },[]) 49 | 50 | const handleProductUpdate=(data)=>{ 51 | const productUpdate={...data,_id:selectedProduct._id,images:[data?.image0,data?.image1,data?.image2,data?.image3]} 52 | delete productUpdate?.image0 53 | delete productUpdate?.image1 54 | delete productUpdate?.image2 55 | delete productUpdate?.image3 56 | 57 | dispatch(updateProductByIdAsync(productUpdate)) 58 | } 59 | 60 | 61 | return ( 62 | 63 | 64 | { 65 | selectedProduct && 66 | 67 | 68 | 69 | {/* feild area */} 70 | 71 | 72 | Title 73 | 74 | 75 | 76 | 77 | 78 | 79 | Brand 80 | 89 | 90 | 91 | 92 | 93 | Category 94 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | Description 110 | 111 | 112 | 113 | 114 | 115 | Price 116 | 117 | 118 | 119 | Discount {is480?"%":"Percentage"} 120 | 121 | 122 | 123 | 124 | 125 | Stock Quantity 126 | 127 | 128 | 129 | Thumbnail 130 | 131 | 132 | 133 | 134 | Product Images 135 | 136 | 137 | { 138 | selectedProduct.images.map((image,index)=>( 139 | 140 | )) 141 | } 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | {/* action area */} 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | } 158 | 159 | 160 | ) 161 | } 162 | -------------------------------------------------------------------------------- /frontend/src/features/auth/AuthApi.jsx: -------------------------------------------------------------------------------- 1 | import {axiosi} from '../../config/axios' 2 | 3 | export const signup=async(cred)=>{ 4 | try { 5 | const res=await axiosi.post("auth/signup",cred) 6 | return res.data 7 | } catch (error) { 8 | throw error.response.data 9 | } 10 | } 11 | export const login=async(cred)=>{ 12 | try { 13 | const res=await axiosi.post("auth/login",cred) 14 | return res.data 15 | } catch (error) { 16 | throw error.response.data 17 | } 18 | } 19 | export const verifyOtp=async(cred)=>{ 20 | try { 21 | const res=await axiosi.post("auth/verify-otp",cred) 22 | return res.data 23 | } catch (error) { 24 | throw error.response.data 25 | } 26 | } 27 | export const resendOtp=async(cred)=>{ 28 | try { 29 | const res=await axiosi.post("auth/resend-otp",cred) 30 | return res.data 31 | } catch (error) { 32 | throw error.response.data 33 | } 34 | } 35 | export const forgotPassword=async(cred)=>{ 36 | try { 37 | const res=await axiosi.post("auth/forgot-password",cred) 38 | return res.data 39 | } catch (error) { 40 | throw error.response.data 41 | } 42 | } 43 | export const resetPassword=async(cred)=>{ 44 | try { 45 | const res=await axiosi.post("auth/reset-password",cred) 46 | return res.data 47 | } catch (error) { 48 | throw error.response.data 49 | } 50 | } 51 | export const checkAuth=async(cred)=>{ 52 | try { 53 | const res=await axiosi.get("auth/check-auth") 54 | return res.data 55 | } catch (error) { 56 | throw error.response.data 57 | } 58 | } 59 | export const logout=async()=>{ 60 | try { 61 | const res=await axiosi.get("auth/logout") 62 | return res.data 63 | } catch (error) { 64 | throw error.response.data 65 | } 66 | } -------------------------------------------------------------------------------- /frontend/src/features/auth/components/ForgotPassword.jsx: -------------------------------------------------------------------------------- 1 | import { FormHelperText, Paper, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material' 2 | import React, { useEffect } from 'react' 3 | import { toast } from 'react-toastify' 4 | import { useForm } from "react-hook-form" 5 | import { useDispatch, useSelector } from 'react-redux' 6 | import { clearForgotPasswordError, clearForgotPasswordSuccessMessage, forgotPasswordAsync,resetForgotPasswordStatus,selectForgotPasswordError, selectForgotPasswordStatus, selectForgotPasswordSuccessMessage } from '../AuthSlice' 7 | import { LoadingButton } from '@mui/lab' 8 | import { Link } from 'react-router-dom' 9 | import {motion} from 'framer-motion' 10 | 11 | export const ForgotPassword = () => { 12 | const {register,handleSubmit,reset,formState: { errors }} = useForm() 13 | const dispatch=useDispatch() 14 | const status=useSelector(selectForgotPasswordStatus) 15 | const error=useSelector(selectForgotPasswordError) 16 | const successMessage=useSelector(selectForgotPasswordSuccessMessage) 17 | const theme=useTheme() 18 | const is500=useMediaQuery(theme.breakpoints.down(500)) 19 | 20 | useEffect(()=>{ 21 | if(error){ 22 | toast.error(error?.message) 23 | } 24 | return ()=>{ 25 | dispatch(clearForgotPasswordError()) 26 | } 27 | },[error]) 28 | 29 | useEffect(()=>{ 30 | if(status==='fullfilled'){ 31 | toast.success(successMessage?.message) 32 | } 33 | return ()=>{ 34 | dispatch(clearForgotPasswordSuccessMessage()) 35 | } 36 | },[status]) 37 | 38 | useEffect(()=>{ 39 | return ()=>{ 40 | dispatch(resetForgotPasswordStatus()) 41 | } 42 | },[]) 43 | 44 | const handleForgotPassword=async(data)=>{ 45 | dispatch(forgotPasswordAsync(data)) 46 | reset() 47 | } 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {status==='fullfilled'?"Email has been sent!":"Forgot Your Password?"} 58 | {status==='fullfilled'?"Please check your inbox and click on the received link to reset your password":"Enter your registered email below to receive password reset link"} 59 | 60 | 61 | { 62 | status!=='fullfilled' && 63 | <> 64 | 65 | 66 | {errors.email && {errors.email.message}} 67 | 68 | 69 | 70 | Send Password Reset Link 71 | 72 | 73 | } 74 | 75 | 76 | 77 | {/* back to login navigation */} 78 | 79 | Go back to login 80 | 81 | 82 | 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /frontend/src/features/auth/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import {Box, FormHelperText, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material' 2 | import React, { useEffect } from 'react' 3 | import Lottie from 'lottie-react' 4 | import { Link, useNavigate } from 'react-router-dom' 5 | import { useForm } from "react-hook-form" 6 | import { ecommerceOutlookAnimation, shoppingBagAnimation} from '../../../assets' 7 | import {useDispatch,useSelector} from 'react-redux' 8 | import { LoadingButton } from '@mui/lab'; 9 | import {selectLoggedInUser,loginAsync,selectLoginStatus, selectLoginError, clearLoginError, resetLoginStatus} from '../AuthSlice' 10 | import { toast } from 'react-toastify' 11 | import {MotionConfig, motion} from 'framer-motion' 12 | 13 | export const Login = () => { 14 | const dispatch=useDispatch() 15 | const status=useSelector(selectLoginStatus) 16 | const error=useSelector(selectLoginError) 17 | const loggedInUser=useSelector(selectLoggedInUser) 18 | const {register,handleSubmit,reset,formState: { errors }} = useForm() 19 | const navigate=useNavigate() 20 | const theme=useTheme() 21 | const is900=useMediaQuery(theme.breakpoints.down(900)) 22 | const is480=useMediaQuery(theme.breakpoints.down(480)) 23 | 24 | // handles user redirection 25 | useEffect(()=>{ 26 | if(loggedInUser && loggedInUser?.isVerified){ 27 | navigate("/") 28 | } 29 | else if(loggedInUser && !loggedInUser?.isVerified){ 30 | navigate("/verify-otp") 31 | } 32 | },[loggedInUser]) 33 | 34 | // handles login error and toast them 35 | useEffect(()=>{ 36 | if(error){ 37 | toast.error(error.message) 38 | } 39 | },[error]) 40 | 41 | // handles login status and dispatches reset actions to relevant states in cleanup 42 | useEffect(()=>{ 43 | if(status==='fullfilled' && loggedInUser?.isVerified===true){ 44 | toast.success(`Login successful`) 45 | reset() 46 | } 47 | return ()=>{ 48 | dispatch(clearLoginError()) 49 | dispatch(resetLoginStatus()) 50 | } 51 | },[status]) 52 | 53 | const handleLogin=(data)=>{ 54 | const cred={...data} 55 | delete cred.confirmPassword 56 | dispatch(loginAsync(cred)) 57 | } 58 | 59 | return ( 60 | 61 | 62 | { 63 | !is900 && 64 | 65 | 66 | 67 | 68 | } 69 | 70 | 71 | 72 | 73 | 74 | 75 | Mern Shop 76 | - Shop Anything 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {errors.email && {errors.email.message}} 86 | 87 | 88 | 89 | 90 | 91 | {errors.password && {errors.password.message}} 92 | 93 | 94 | 95 | Login 96 | 97 | 98 | 99 | 100 | 101 | 102 | Forgot password 103 | 104 | 105 | 106 | Don't have an account? Register 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /frontend/src/features/auth/components/Logout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { logoutAsync, selectLoggedInUser } from '../AuthSlice' 4 | import { useNavigate } from 'react-router-dom' 5 | 6 | export const Logout = () => { 7 | const dispatch=useDispatch() 8 | const loggedInUser=useSelector(selectLoggedInUser) 9 | const navigate=useNavigate() 10 | 11 | useEffect(()=>{ 12 | dispatch(logoutAsync()) 13 | },[]) 14 | 15 | useEffect(()=>{ 16 | if(!loggedInUser){ 17 | navigate("/login") 18 | } 19 | },[loggedInUser]) 20 | 21 | return ( 22 | <> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/features/auth/components/OtpVerfication.jsx: -------------------------------------------------------------------------------- 1 | import {Button, FormHelperText, Paper, Stack, TextField, Typography } from '@mui/material' 2 | import React, { useEffect} from 'react' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { clearOtpVerificationError, clearResendOtpError, clearResendOtpSuccessMessage, resendOtpAsync, resetOtpVerificationStatus, resetResendOtpStatus, selectLoggedInUser, selectOtpVerificationError, selectOtpVerificationStatus, selectResendOtpError, selectResendOtpStatus, selectResendOtpSuccessMessage, verifyOtpAsync } from '../AuthSlice' 5 | import { LoadingButton } from '@mui/lab' 6 | import { useNavigate } from 'react-router-dom' 7 | import { useForm } from "react-hook-form" 8 | import {toast} from 'react-toastify' 9 | 10 | 11 | export const OtpVerfication = () => { 12 | 13 | const {register,handleSubmit,formState: { errors }} = useForm() 14 | const dispatch=useDispatch() 15 | const loggedInUser=useSelector(selectLoggedInUser) 16 | const navigate=useNavigate() 17 | const resendOtpStatus=useSelector(selectResendOtpStatus) 18 | const resendOtpError=useSelector(selectResendOtpError) 19 | const resendOtpSuccessMessage=useSelector(selectResendOtpSuccessMessage) 20 | const otpVerificationStatus=useSelector(selectOtpVerificationStatus) 21 | const otpVerificationError=useSelector(selectOtpVerificationError) 22 | 23 | // handles the redirection 24 | useEffect(()=>{ 25 | if(!loggedInUser){ 26 | navigate('/login') 27 | } 28 | else if(loggedInUser && loggedInUser?.isVerified){ 29 | navigate("/") 30 | } 31 | },[loggedInUser]) 32 | 33 | const handleSendOtp=()=>{ 34 | const data={user:loggedInUser?._id} 35 | dispatch(resendOtpAsync(data)) 36 | } 37 | 38 | const handleVerifyOtp=(data)=>{ 39 | const cred={...data,userId:loggedInUser?._id} 40 | dispatch(verifyOtpAsync(cred)) 41 | } 42 | 43 | // handles resend otp error 44 | useEffect(()=>{ 45 | if(resendOtpError){ 46 | toast.error(resendOtpError.message) 47 | } 48 | return ()=>{ 49 | dispatch(clearResendOtpError()) 50 | } 51 | },[resendOtpError]) 52 | 53 | // handles resend otp success message 54 | useEffect(()=>{ 55 | if(resendOtpSuccessMessage){ 56 | toast.success(resendOtpSuccessMessage.message) 57 | } 58 | return ()=>{ 59 | dispatch(clearResendOtpSuccessMessage()) 60 | } 61 | },[resendOtpSuccessMessage]) 62 | 63 | // handles error while verifying otp 64 | useEffect(()=>{ 65 | if(otpVerificationError){ 66 | toast.error(otpVerificationError.message) 67 | } 68 | return ()=>{ 69 | dispatch(clearOtpVerificationError()) 70 | } 71 | },[otpVerificationError]) 72 | 73 | useEffect(()=>{ 74 | if(otpVerificationStatus==='fullfilled'){ 75 | toast.success("Email verified! We are happy to have you here") 76 | dispatch(resetResendOtpStatus()) 77 | } 78 | return ()=>{ 79 | dispatch(resetOtpVerificationStatus()) 80 | } 81 | },[otpVerificationStatus]) 82 | 83 | return ( 84 | 85 | 86 | 87 | 88 | 89 | Verify Your Email Address 90 | 91 | { 92 | resendOtpStatus==='fullfilled'?( 93 | 94 | 95 | 96 | Enter the 4 digit OTP sent on 97 | {loggedInUser?.email} 98 | 99 | 100 | 101 | {errors?.otp && {errors.otp.message}} 102 | 103 | 104 | Verify 105 | 106 | ): 107 | <> 108 | 109 | We will send you a OTP on 110 | {loggedInUser?.email} 111 | 112 | Get OTP 113 | 114 | } 115 | 116 | 117 | 118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /frontend/src/features/auth/components/Protected.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux" 2 | import { selectLoggedInUser } from "../AuthSlice" 3 | import { Navigate } from "react-router" 4 | 5 | 6 | export const Protected = ({children}) => { 7 | const loggedInUser=useSelector(selectLoggedInUser) 8 | 9 | if(loggedInUser?.isVerified){ 10 | return children 11 | } 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/features/auth/components/ResetPassword.jsx: -------------------------------------------------------------------------------- 1 | import { FormHelperText, Paper, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material' 2 | import React, { useEffect } from 'react' 3 | import { toast } from 'react-toastify' 4 | import { useForm } from "react-hook-form" 5 | import { useDispatch, useSelector } from 'react-redux' 6 | import {clearResetPasswordError, clearResetPasswordSuccessMessage, resetPasswordAsync, resetResetPasswordStatus, selectResetPasswordError, selectResetPasswordStatus, selectResetPasswordSuccessMessage } from '../AuthSlice' 7 | import { LoadingButton } from '@mui/lab' 8 | import { Link, useNavigate, useParams } from 'react-router-dom' 9 | import {MotionConfig,motion} from 'framer-motion' 10 | 11 | export const ResetPassword = () => { 12 | const {register,handleSubmit,reset,formState: { errors }} = useForm() 13 | const dispatch=useDispatch() 14 | const status=useSelector(selectResetPasswordStatus) 15 | const error=useSelector(selectResetPasswordError) 16 | const successMessage=useSelector(selectResetPasswordSuccessMessage) 17 | const {userId,passwordResetToken}=useParams() 18 | const navigate=useNavigate() 19 | const theme=useTheme() 20 | const is500=useMediaQuery(theme.breakpoints.down(500)) 21 | 22 | useEffect(()=>{ 23 | if(error){ 24 | toast.error(error.message) 25 | } 26 | return ()=>{ 27 | dispatch(clearResetPasswordError()) 28 | } 29 | },[error]) 30 | 31 | useEffect(()=>{ 32 | if(status==='fullfilled'){ 33 | toast.success(successMessage?.message) 34 | navigate("/login") 35 | } 36 | return ()=>{ 37 | dispatch(clearResetPasswordSuccessMessage()) 38 | } 39 | },[status]) 40 | 41 | useEffect(()=>{ 42 | return ()=>{ 43 | dispatch(resetResetPasswordStatus()) 44 | } 45 | },[]) 46 | 47 | const handleResetPassword=async(data)=>{ 48 | const cred={...data,userId:userId,token:passwordResetToken} 49 | delete cred.confirmPassword 50 | dispatch(resetPasswordAsync(cred)) 51 | reset() 52 | } 53 | 54 | return ( 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Reset Password 64 | Please enter and confirm new password 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {errors.password && {errors.password.message}} 73 | 74 | 75 | 76 | value===formValues.password || "Passwords dosen't match"})} placeholder='Confirm New Password'/> 77 | {errors.confirmPassword && {errors.confirmPassword.message}} 78 | 79 | 80 | 81 | 82 | 83 | 84 | Reset Password 85 | 86 | 87 | 88 | 89 | 90 | 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /frontend/src/features/auth/components/Signup.jsx: -------------------------------------------------------------------------------- 1 | import {FormHelperText, Stack, TextField, Typography,Box, useTheme, useMediaQuery} from '@mui/material' 2 | import React, { useEffect } from 'react' 3 | import Lottie from 'lottie-react' 4 | import { Link, useNavigate } from 'react-router-dom' 5 | import { useForm } from "react-hook-form" 6 | import { ecommerceOutlookAnimation, shoppingBagAnimation} from '../../../assets' 7 | import {useDispatch,useSelector} from 'react-redux' 8 | import { LoadingButton } from '@mui/lab'; 9 | import {selectLoggedInUser, signupAsync,selectSignupStatus, selectSignupError, clearSignupError, resetSignupStatus} from '../AuthSlice' 10 | import { toast } from 'react-toastify' 11 | import { MotionConfig , motion} from 'framer-motion' 12 | 13 | export const Signup = () => { 14 | const dispatch=useDispatch() 15 | const status=useSelector(selectSignupStatus) 16 | const error=useSelector(selectSignupError) 17 | const loggedInUser=useSelector(selectLoggedInUser) 18 | const {register,handleSubmit,reset,formState: { errors }} = useForm() 19 | const navigate=useNavigate() 20 | const theme=useTheme() 21 | const is900=useMediaQuery(theme.breakpoints.down(900)) 22 | const is480=useMediaQuery(theme.breakpoints.down(480)) 23 | 24 | // handles user redirection 25 | useEffect(()=>{ 26 | if(loggedInUser && !loggedInUser?.isVerified){ 27 | navigate("/verify-otp") 28 | } 29 | else if(loggedInUser){ 30 | navigate("/") 31 | } 32 | },[loggedInUser]) 33 | 34 | 35 | // handles signup error and toast them 36 | useEffect(()=>{ 37 | if(error){ 38 | toast.error(error.message) 39 | } 40 | },[error]) 41 | 42 | 43 | useEffect(()=>{ 44 | if(status==='fullfilled'){ 45 | toast.success("Welcome! Verify your email to start shopping on mern-ecommerce.") 46 | reset() 47 | } 48 | return ()=>{ 49 | dispatch(clearSignupError()) 50 | dispatch(resetSignupStatus()) 51 | } 52 | },[status]) 53 | 54 | // this function handles signup and dispatches the signup action with credentails that api requires 55 | const handleSignup=(data)=>{ 56 | const cred={...data} 57 | delete cred.confirmPassword 58 | dispatch(signupAsync(cred)) 59 | } 60 | 61 | return ( 62 | 63 | 64 | { 65 | !is900 && 66 | 67 | 68 | 69 | 70 | 71 | } 72 | 73 | 74 | 75 | 76 | 77 | Mern Shop 78 | - Shop Anything 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | {errors.name && {errors.name.message}} 90 | 91 | 92 | 93 | 94 | {errors.email && {errors.email.message}} 95 | 96 | 97 | 98 | 99 | {errors.password && {errors.password.message}} 100 | 101 | 102 | 103 | value===fromValues.password || "Passwords doesn't match"})} placeholder='Confirm Password'/> 104 | {errors.confirmPassword && {errors.confirmPassword.message}} 105 | 106 | 107 | 108 | 109 | 110 | Signup 111 | 112 | 113 | 114 | 115 | 116 | Forgot password 117 | 118 | 119 | 120 | Already a member? Login 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ) 131 | } 132 | -------------------------------------------------------------------------------- /frontend/src/features/brands/BrandApi.jsx: -------------------------------------------------------------------------------- 1 | import { axiosi } from "../../config/axios"; 2 | 3 | export const fetchAllBrands=async()=>{ 4 | try { 5 | const res=await axiosi.get("/brands") 6 | return res.data 7 | } catch (error) { 8 | throw error.response.data 9 | } 10 | } -------------------------------------------------------------------------------- /frontend/src/features/brands/BrandSlice.jsx: -------------------------------------------------------------------------------- 1 | import {createAsyncThunk, createSlice} from '@reduxjs/toolkit' 2 | import { fetchAllBrands } from './BrandApi' 3 | 4 | const initialState={ 5 | status:"idle", 6 | brands:[], 7 | errors:null 8 | } 9 | 10 | export const fetchAllBrandsAsync=createAsyncThunk('brands/fetchAllBrandsAsync',async()=>{ 11 | const brands=await fetchAllBrands() 12 | return brands 13 | }) 14 | 15 | const brandSlice=createSlice({ 16 | name:"brandSlice", 17 | initialState:initialState, 18 | reducers:{}, 19 | extraReducers:(builder)=>{ 20 | builder 21 | .addCase(fetchAllBrandsAsync.pending,(state)=>{ 22 | state.status='idle' 23 | }) 24 | .addCase(fetchAllBrandsAsync.fulfilled,(state,action)=>{ 25 | state.status='fulfilled' 26 | state.brands=action.payload 27 | }) 28 | .addCase(fetchAllBrandsAsync.rejected,(state,action)=>{ 29 | state.status='rejected' 30 | state.errors=action.error 31 | }) 32 | 33 | } 34 | }) 35 | 36 | // exporting selectors 37 | export const selectBrandStatus=(state)=>state.BrandSlice.status 38 | export const selectBrands=(state)=>state.BrandSlice.brands 39 | export const selectBrandErrors=(state)=>state.BrandSlice.errors 40 | 41 | export default brandSlice.reducer -------------------------------------------------------------------------------- /frontend/src/features/cart/CartApi.jsx: -------------------------------------------------------------------------------- 1 | import {axiosi} from '../../config/axios' 2 | 3 | export const addToCart=async(item)=>{ 4 | try { 5 | const res=await axiosi.post('/cart',item) 6 | return res.data 7 | } catch (error) { 8 | throw error.response.data 9 | } 10 | } 11 | export const fetchCartByUserId=async(id)=>{ 12 | try { 13 | const res=await axiosi.get(`/cart/user/${id}`) 14 | return res.data 15 | } catch (error) { 16 | throw error.response.data 17 | } 18 | } 19 | export const updateCartItemById=async(update)=>{ 20 | try { 21 | const res=await axiosi.patch(`/cart/${update._id}`,update) 22 | return res.data 23 | } catch (error) { 24 | throw error.response.data 25 | } 26 | } 27 | export const deleteCartItemById=async(id)=>{ 28 | try { 29 | const res=await axiosi.delete(`/cart/${id}`) 30 | return res.data 31 | } catch (error) { 32 | throw error.response.data 33 | } 34 | } 35 | 36 | export const resetCartByUserId=async(userId)=>{ 37 | try { 38 | const res=await axiosi.delete(`/cart/user/${userId}`) 39 | return res.data 40 | } catch (error) { 41 | throw error.response.data 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/features/cart/CartSlice.jsx: -------------------------------------------------------------------------------- 1 | import {createAsyncThunk, createSlice} from '@reduxjs/toolkit' 2 | import {addToCart,fetchCartByUserId,updateCartItemById,deleteCartItemById, resetCartByUserId} from './CartApi' 3 | 4 | const initialState={ 5 | status:"idle", 6 | items:[], 7 | cartItemAddStatus:"idle", 8 | cartItemRemoveStatus:"idle", 9 | errors:null, 10 | successMessage:null 11 | } 12 | 13 | export const addToCartAsync=createAsyncThunk('cart/addToCartAsync',async(item)=>{ 14 | const addedItem=await addToCart(item) 15 | return addedItem 16 | }) 17 | export const fetchCartByUserIdAsync=createAsyncThunk('cart/fetchCartByUserIdAsync',async(id)=>{ 18 | const items=await fetchCartByUserId(id) 19 | return items 20 | }) 21 | export const updateCartItemByIdAsync=createAsyncThunk('cart/updateCartItemByIdAsync',async(update)=>{ 22 | const updatedItem=await updateCartItemById(update) 23 | return updatedItem 24 | }) 25 | export const deleteCartItemByIdAsync=createAsyncThunk('cart/deleteCartItemByIdAsync',async(id)=>{ 26 | const deletedItem=await deleteCartItemById(id) 27 | return deletedItem 28 | }) 29 | export const resetCartByUserIdAsync=createAsyncThunk('cart/resetCartByUserIdAsync',async(userId)=>{ 30 | const updatedCart=await resetCartByUserId(userId) 31 | return updatedCart 32 | }) 33 | 34 | const cartSlice=createSlice({ 35 | name:"cartSlice", 36 | initialState:initialState, 37 | reducers:{ 38 | resetCartItemAddStatus:(state)=>{ 39 | state.cartItemAddStatus='idle' 40 | }, 41 | resetCartItemRemoveStatus:(state)=>{ 42 | state.cartItemRemoveStatus='idle' 43 | } 44 | }, 45 | extraReducers:(builder)=>{ 46 | builder 47 | .addCase(addToCartAsync.pending,(state)=>{ 48 | state.cartItemAddStatus='pending' 49 | }) 50 | .addCase(addToCartAsync.fulfilled,(state,action)=>{ 51 | state.cartItemAddStatus='fulfilled' 52 | state.items.push(action.payload) 53 | }) 54 | .addCase(addToCartAsync.rejected,(state,action)=>{ 55 | state.cartItemAddStatus='rejected' 56 | state.errors=action.error 57 | }) 58 | 59 | .addCase(fetchCartByUserIdAsync.pending,(state)=>{ 60 | state.status='pending' 61 | }) 62 | .addCase(fetchCartByUserIdAsync.fulfilled,(state,action)=>{ 63 | state.status='fulfilled' 64 | state.items=action.payload 65 | }) 66 | .addCase(fetchCartByUserIdAsync.rejected,(state,action)=>{ 67 | state.status='rejected' 68 | state.errors=action.error 69 | }) 70 | 71 | .addCase(updateCartItemByIdAsync.pending,(state)=>{ 72 | state.status='pending' 73 | }) 74 | .addCase(updateCartItemByIdAsync.fulfilled,(state,action)=>{ 75 | state.status='fulfilled' 76 | const index=state.items.findIndex((item)=>item._id===action.payload._id) 77 | state.items[index]=action.payload 78 | }) 79 | .addCase(updateCartItemByIdAsync.rejected,(state,action)=>{ 80 | state.status='rejected' 81 | state.errors=action.error 82 | }) 83 | 84 | .addCase(deleteCartItemByIdAsync.pending,(state)=>{ 85 | state.cartItemRemoveStatus='pending' 86 | }) 87 | .addCase(deleteCartItemByIdAsync.fulfilled,(state,action)=>{ 88 | state.cartItemRemoveStatus='fulfilled' 89 | state.items=state.items.filter((item)=>item._id!==action.payload._id) 90 | }) 91 | .addCase(deleteCartItemByIdAsync.rejected,(state,action)=>{ 92 | state.cartItemRemoveStatus='rejected' 93 | state.errors=action.error 94 | }) 95 | 96 | .addCase(resetCartByUserIdAsync.pending,(state)=>{ 97 | state.status='pending' 98 | }) 99 | .addCase(resetCartByUserIdAsync.fulfilled,(state)=>{ 100 | state.status='fulfilled' 101 | state.items=[] 102 | }) 103 | .addCase(resetCartByUserIdAsync.rejected,(state,action)=>{ 104 | state.status='rejected' 105 | state.errors=action.error 106 | }) 107 | } 108 | }) 109 | 110 | // exporting selectors 111 | export const selectCartStatus=(state)=>state.CartSlice.status 112 | export const selectCartItems=(state)=>state.CartSlice.items 113 | export const selectCartErrors=(state)=>state.CartSlice.errors 114 | export const selectCartSuccessMessage=(state)=>state.CartSlice.successMessage 115 | export const selectCartItemAddStatus=(state)=>state.CartSlice.cartItemAddStatus 116 | export const selectCartItemRemoveStatus=(state)=>state.CartSlice.cartItemRemoveStatus 117 | 118 | // exporting reducers 119 | export const {resetCartItemAddStatus,resetCartItemRemoveStatus}=cartSlice.actions 120 | 121 | export default cartSlice.reducer -------------------------------------------------------------------------------- /frontend/src/features/cart/components/Cart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { CartItem } from './CartItem' 3 | import { Button, Chip, Paper, Stack, Typography, useMediaQuery, useTheme } from '@mui/material' 4 | import { resetCartItemRemoveStatus, selectCartItemRemoveStatus, selectCartItems } from '../CartSlice' 5 | import { useDispatch, useSelector } from 'react-redux' 6 | import { Link, useNavigate } from 'react-router-dom' 7 | import { SHIPPING, TAXES } from '../../../constants' 8 | import { toast } from 'react-toastify' 9 | import {motion} from 'framer-motion' 10 | 11 | export const Cart = ({checkout}) => { 12 | const items=useSelector(selectCartItems) 13 | const subtotal=items.reduce((acc,item)=>item.product.price*item.quantity+acc,0) 14 | const totalItems=items.reduce((acc,item)=>acc+item.quantity,0) 15 | const navigate=useNavigate() 16 | const theme=useTheme() 17 | const is900=useMediaQuery(theme.breakpoints.down(900)) 18 | 19 | const cartItemRemoveStatus=useSelector(selectCartItemRemoveStatus) 20 | const dispatch=useDispatch() 21 | 22 | useEffect(()=>{ 23 | window.scrollTo({ 24 | top:0, 25 | behavior:"instant" 26 | }) 27 | },[]) 28 | 29 | useEffect(()=>{ 30 | if(items.length===0){ 31 | navigate("/") 32 | } 33 | },[items]) 34 | 35 | useEffect(()=>{ 36 | if(cartItemRemoveStatus==='fulfilled'){ 37 | toast.success("Product removed from cart") 38 | } 39 | else if(cartItemRemoveStatus==='rejected'){ 40 | toast.error("Error removing product from cart, please try again later") 41 | } 42 | },[cartItemRemoveStatus]) 43 | 44 | useEffect(()=>{ 45 | return ()=>{ 46 | dispatch(resetCartItemRemoveStatus()) 47 | } 48 | },[]) 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | {/* cart items */} 56 | 57 | { 58 | items && items.map((item)=>( 59 | 60 | )) 61 | } 62 | 63 | 64 | {/* subtotal */} 65 | 66 | 67 | { 68 | checkout?( 69 | 70 | 71 | 72 | Subtotal 73 | ${subtotal} 74 | 75 | 76 | 77 | Shipping 78 | ${SHIPPING} 79 | 80 | 81 | 82 | Taxes 83 | ${TAXES} 84 | 85 | 86 |
87 | 88 | 89 | Total 90 | ${subtotal+SHIPPING+TAXES} 91 | 92 | 93 | 94 |
95 | ):( 96 | <> 97 | 98 | Subtotal 99 | Total items in cart {totalItems} 100 | Shipping and taxes will be calculated at checkout. 101 | 102 | 103 | 104 | ${subtotal} 105 | 106 | 107 | ) 108 | } 109 | 110 |
111 | 112 | {/* checkout or continue shopping */} 113 | { 114 | !checkout && 115 | 116 | 117 | 118 | 119 | } 120 | 121 |
122 | 123 | 124 |
125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /frontend/src/features/cart/components/CartItem.jsx: -------------------------------------------------------------------------------- 1 | import { Button, IconButton, Paper, Stack, Typography, useMediaQuery, useTheme } from '@mui/material' 2 | import React from 'react' 3 | import AddIcon from '@mui/icons-material/Add'; 4 | import RemoveIcon from '@mui/icons-material/Remove'; 5 | import { useDispatch } from 'react-redux'; 6 | import { deleteCartItemByIdAsync, updateCartItemByIdAsync } from '../CartSlice'; 7 | import { Link } from 'react-router-dom'; 8 | 9 | export const CartItem = ({id,thumbnail,title,category,brand,price,quantity,stockQuantity,productId}) => { 10 | 11 | 12 | const dispatch=useDispatch() 13 | const theme=useTheme() 14 | const is900=useMediaQuery(theme.breakpoints.down(900)) 15 | const is480=useMediaQuery(theme.breakpoints.down(480)) 16 | const is552=useMediaQuery(theme.breakpoints.down(552)) 17 | 18 | const handleAddQty=()=>{ 19 | const update={_id:id,quantity:quantity+1} 20 | dispatch(updateCartItemByIdAsync(update)) 21 | } 22 | const handleRemoveQty=()=>{ 23 | if(quantity===1){ 24 | dispatch(deleteCartItemByIdAsync(id)) 25 | } 26 | else{ 27 | const update={_id:id,quantity:quantity-1} 28 | dispatch(updateCartItemByIdAsync(update)) 29 | } 30 | } 31 | 32 | const handleProductRemove=()=>{ 33 | dispatch(deleteCartItemByIdAsync(id)) 34 | } 35 | 36 | 37 | return ( 38 | 39 | 40 | {/* image and details */} 41 | 42 | 43 | 44 | {`${title} 45 | 46 | 47 | 48 | {title} 49 | {brand} 50 | Quantity 51 | 52 | 53 | {quantity} 54 | 55 | 56 | 57 | 58 | 59 | {/* price and remove button */} 60 | 61 | ${price} 62 | 63 | 64 | 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/features/categories/CategoriesApi.jsx: -------------------------------------------------------------------------------- 1 | import { axiosi } from "../../config/axios" 2 | 3 | export const fetchAllCategories=async()=>{ 4 | try { 5 | const res=await axiosi.get("/categories") 6 | return res.data 7 | } catch (error) { 8 | throw error.response.data 9 | } 10 | } -------------------------------------------------------------------------------- /frontend/src/features/categories/CategoriesSlice.jsx: -------------------------------------------------------------------------------- 1 | import {createAsyncThunk, createSlice} from '@reduxjs/toolkit' 2 | import { fetchAllCategories } from './CategoriesApi' 3 | 4 | const initialState={ 5 | status:"idle", 6 | categories:[], 7 | errors:null 8 | } 9 | 10 | export const fetchAllCategoriesAsync=createAsyncThunk('categories/fetchAllCategoriesAsync',async()=>{ 11 | const categories=await fetchAllCategories() 12 | return categories 13 | }) 14 | 15 | const categorySlice=createSlice({ 16 | name:"categorySlice", 17 | initialState:initialState, 18 | reducers:{}, 19 | extraReducers:(builder)=>{ 20 | builder 21 | .addCase(fetchAllCategoriesAsync.pending,(state)=>{ 22 | state.status='idle' 23 | }) 24 | .addCase(fetchAllCategoriesAsync.fulfilled,(state,action)=>{ 25 | state.status='fulfilled' 26 | state.categories=action.payload 27 | }) 28 | .addCase(fetchAllCategoriesAsync.rejected,(state,action)=>{ 29 | state.status='rejected' 30 | state.errors=action.error 31 | }) 32 | 33 | } 34 | }) 35 | 36 | // exporting selectors 37 | export const selectCategoryStatus=(state)=>state.CategoriesSlice.status 38 | export const selectCategories=(state)=>state.CategoriesSlice.categories 39 | export const selectCategoryErrors=(state)=>state.CategoriesSlice.errors 40 | 41 | export default categorySlice.reducer -------------------------------------------------------------------------------- /frontend/src/features/footer/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { Box, IconButton, TextField, Typography, useMediaQuery, useTheme } from '@mui/material' 2 | import { Stack } from '@mui/material' 3 | import React from 'react' 4 | import { QRCodePng, appStorePng, googlePlayPng ,facebookPng,instagramPng,twitterPng,linkedinPng} from '../../assets' 5 | import SendIcon from '@mui/icons-material/Send'; 6 | import { MotionConfig, motion } from 'framer-motion'; 7 | import { Link } from 'react-router-dom'; 8 | 9 | 10 | 11 | export const Footer = () => { 12 | 13 | const theme=useTheme() 14 | const is700=useMediaQuery(theme.breakpoints.down(700)) 15 | 16 | const labelStyles={ 17 | fontWeight:300, 18 | cursor:'pointer' 19 | } 20 | 21 | return ( 22 | 23 | 24 | {/* upper */} 25 | 26 | 27 | 28 | Exclusive 29 | Subscribe 30 | Get 10% off your first order 31 | ,style:{color:"whitesmoke"}}}/> 32 | 33 | 34 | 35 | Support 36 | 11th Main Street, Dhaka, DH 1515, California. 37 | exclusive@gmail.com 38 | +88015-88888-9999 39 | 40 | 41 | 42 | Account 43 | My Account 44 | Login / Register 45 | Cart 46 | Wishlist 47 | Shop 48 | 49 | 50 | 51 | Quick Links 52 | Privacy Policy 53 | Terms Of Use 54 | FAQ 55 | Contact 56 | 57 | 58 | 59 | Download App 60 | Save $3 with App New User Only 61 | 62 | 63 | 64 | QR Code 65 | 66 | 67 | 68 | 69 | GooglePlay 70 | 71 | 72 | AppStore 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | {/* lower */} 90 | 91 | © Mern Store {new Date().getFullYear()}. All right reserved 92 | 93 | 94 | 95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /frontend/src/features/navigation/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import AppBar from '@mui/material/AppBar'; 3 | import Toolbar from '@mui/material/Toolbar'; 4 | import IconButton from '@mui/material/IconButton'; 5 | import Typography from '@mui/material/Typography'; 6 | import Menu from '@mui/material/Menu'; 7 | import Avatar from '@mui/material/Avatar'; 8 | import Tooltip from '@mui/material/Tooltip'; 9 | import MenuItem from '@mui/material/MenuItem'; 10 | import { Link, useNavigate } from 'react-router-dom'; 11 | import { Badge, Button, Chip, Stack, useMediaQuery, useTheme } from '@mui/material'; 12 | import { useDispatch, useSelector } from 'react-redux'; 13 | import { selectUserInfo } from '../../user/UserSlice'; 14 | import ShoppingCartOutlinedIcon from '@mui/icons-material/ShoppingCartOutlined'; 15 | import { selectCartItems } from '../../cart/CartSlice'; 16 | import { selectLoggedInUser } from '../../auth/AuthSlice'; 17 | import { selectWishlistItems } from '../../wishlist/WishlistSlice'; 18 | import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; 19 | import TuneIcon from '@mui/icons-material/Tune'; 20 | import { selectProductIsFilterOpen, toggleFilters } from '../../products/ProductSlice'; 21 | 22 | 23 | 24 | export const Navbar=({isProductList=false})=> { 25 | const [anchorElNav, setAnchorElNav] = React.useState(null); 26 | const [anchorElUser, setAnchorElUser] = React.useState(null); 27 | const userInfo=useSelector(selectUserInfo) 28 | const cartItems=useSelector(selectCartItems) 29 | const loggedInUser=useSelector(selectLoggedInUser) 30 | const navigate=useNavigate() 31 | const dispatch=useDispatch() 32 | const theme=useTheme() 33 | const is480=useMediaQuery(theme.breakpoints.down(480)) 34 | 35 | const wishlistItems=useSelector(selectWishlistItems) 36 | const isProductFilterOpen=useSelector(selectProductIsFilterOpen) 37 | 38 | const handleOpenUserMenu = (event) => { 39 | setAnchorElUser(event.currentTarget); 40 | }; 41 | 42 | const handleCloseUserMenu = () => { 43 | setAnchorElUser(null); 44 | }; 45 | 46 | const handleToggleFilters=()=>{ 47 | dispatch(toggleFilters()) 48 | } 49 | 50 | const settings = [ 51 | {name:"Home",to:"/"}, 52 | {name:'Profile',to:loggedInUser?.isAdmin?"/admin/profile":"/profile"}, 53 | {name:loggedInUser?.isAdmin?'Orders':'My orders',to:loggedInUser?.isAdmin?"/admin/orders":"/orders"}, 54 | {name:'Logout',to:"/logout"}, 55 | ]; 56 | 57 | return ( 58 | 59 | 60 | 61 | 62 | MERN SHOP 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 89 | 90 | { 91 | loggedInUser?.isAdmin && 92 | 93 | 94 | Add new Product 95 | 96 | 97 | } 98 | {settings.map((setting) => ( 99 | 100 | {setting.name} 101 | 102 | ))} 103 | 104 | {is480?`${userInfo?.name.toString().split(" ")[0]}`:`Hey👋, ${userInfo?.name}`} 105 | {loggedInUser.isAdmin && } 106 | 107 | 108 | 109 | { 110 | cartItems?.length>0 && 111 | 112 | navigate("/cart")}> 113 | 114 | 115 | 116 | } 117 | 118 | { 119 | !loggedInUser?.isAdmin && 120 | 121 | 122 | 123 | 124 | 125 | } 126 | { 127 | isProductList && 128 | } 129 | 130 | 131 | 132 | 133 | 134 | ); 135 | } -------------------------------------------------------------------------------- /frontend/src/features/order/OrderApi.jsx: -------------------------------------------------------------------------------- 1 | import {axiosi} from '../../config/axios' 2 | 3 | 4 | export const createOrder=async(order)=>{ 5 | try { 6 | const res=await axiosi.post("/orders",order) 7 | return res.data 8 | } catch (error) { 9 | throw error.response.data 10 | } 11 | } 12 | 13 | export const getOrderByUserId=async(id)=>{ 14 | try { 15 | const res=await axiosi.get(`/orders/user/${id}`) 16 | return res.data 17 | } catch (error) { 18 | throw error.response.data 19 | } 20 | } 21 | 22 | export const getAllOrders=async()=>{ 23 | try { 24 | const res=await axiosi.get(`/orders`) 25 | return res.data 26 | } catch (error) { 27 | throw error.response.data 28 | } 29 | } 30 | 31 | export const updateOrderById=async(update)=>{ 32 | try { 33 | const res=await axiosi.patch(`/orders/${update._id}`,update) 34 | return res.data 35 | } catch (error) { 36 | throw error.response.data 37 | } 38 | } -------------------------------------------------------------------------------- /frontend/src/features/order/OrderSlice.jsx: -------------------------------------------------------------------------------- 1 | import {createAsyncThunk, createSlice} from '@reduxjs/toolkit' 2 | import { createOrder, getAllOrders, getOrderByUserId, updateOrderById } from './OrderApi' 3 | 4 | 5 | const initialState={ 6 | status:"idle", 7 | orderUpdateStatus:"idle", 8 | orderFetchStatus:"idle", 9 | orders:[], 10 | currentOrder:null, 11 | errors:null, 12 | successMessage:null 13 | } 14 | 15 | export const createOrderAsync=createAsyncThunk("orders/createOrderAsync",async(order)=>{ 16 | const createdOrder=await createOrder(order) 17 | return createdOrder 18 | }) 19 | 20 | export const getAllOrdersAsync=createAsyncThunk("orders/getAllOrdersAsync",async()=>{ 21 | const orders=await getAllOrders() 22 | return orders 23 | }) 24 | 25 | export const getOrderByUserIdAsync=createAsyncThunk("orders/getOrderByUserIdAsync",async(id)=>{ 26 | const orders=await getOrderByUserId(id) 27 | return orders 28 | }) 29 | 30 | export const updateOrderByIdAsync=createAsyncThunk("orders/updateOrderByIdAsync",async(update)=>{ 31 | const updatedOrder=await updateOrderById(update) 32 | return updatedOrder 33 | }) 34 | 35 | const orderSlice=createSlice({ 36 | name:'orderSlice', 37 | initialState:initialState, 38 | reducers:{ 39 | resetCurrentOrder:(state)=>{ 40 | state.currentOrder=null 41 | }, 42 | resetOrderUpdateStatus:(state)=>{ 43 | state.orderUpdateStatus='idle' 44 | }, 45 | resetOrderFetchStatus:(state)=>{ 46 | state.orderFetchStatus='idle' 47 | } 48 | }, 49 | extraReducers:(builder)=>{ 50 | builder 51 | .addCase(createOrderAsync.pending,(state)=>{ 52 | state.status='pending' 53 | }) 54 | .addCase(createOrderAsync.fulfilled,(state,action)=>{ 55 | state.status='fulfilled' 56 | state.orders.push(action.payload) 57 | state.currentOrder=action.payload 58 | }) 59 | .addCase(createOrderAsync.rejected,(state,action)=>{ 60 | state.status='rejected' 61 | state.errors=action.error 62 | }) 63 | 64 | .addCase(getAllOrdersAsync.pending,(state)=>{ 65 | state.orderFetchStatus='pending' 66 | }) 67 | .addCase(getAllOrdersAsync.fulfilled,(state,action)=>{ 68 | state.orderFetchStatus='fulfilled' 69 | state.orders=action.payload 70 | }) 71 | .addCase(getAllOrdersAsync.rejected,(state,action)=>{ 72 | state.orderFetchStatus='rejected' 73 | state.errors=action.error 74 | }) 75 | 76 | .addCase(getOrderByUserIdAsync.pending,(state)=>{ 77 | state.orderFetchStatus='pending' 78 | }) 79 | .addCase(getOrderByUserIdAsync.fulfilled,(state,action)=>{ 80 | state.orderFetchStatus='fulfilled' 81 | state.orders=action.payload 82 | }) 83 | .addCase(getOrderByUserIdAsync.rejected,(state,action)=>{ 84 | state.orderFetchStatus='rejected' 85 | state.errors=action.error 86 | }) 87 | 88 | .addCase(updateOrderByIdAsync.pending,(state)=>{ 89 | state.orderUpdateStatus='pending' 90 | }) 91 | .addCase(updateOrderByIdAsync.fulfilled,(state,action)=>{ 92 | state.orderUpdateStatus='fulfilled' 93 | const index=state.orders.findIndex((order)=>order._id===action.payload._id) 94 | state.orders[index]=action.payload 95 | }) 96 | .addCase(updateOrderByIdAsync.rejected,(state,action)=>{ 97 | state.orderUpdateStatus='rejected' 98 | state.errors=action.error 99 | }) 100 | } 101 | }) 102 | 103 | // exporting reducers 104 | export const {resetCurrentOrder,resetOrderUpdateStatus,resetOrderFetchStatus}=orderSlice.actions 105 | 106 | // exporting selectors 107 | export const selectOrderStatus=(state)=>state.OrderSlice.status 108 | export const selectOrders=(state)=>state.OrderSlice.orders 109 | export const selectOrdersErrors=(state)=>state.OrderSlice.errors 110 | export const selectOrdersSuccessMessage=(state)=>state.OrderSlice.successMessage 111 | export const selectCurrentOrder=(state)=>state.OrderSlice.currentOrder 112 | export const selectOrderUpdateStatus=(state)=>state.OrderSlice.orderUpdateStatus 113 | export const selectOrderFetchStatus=(state)=>state.OrderSlice.orderFetchStatus 114 | 115 | export default orderSlice.reducer -------------------------------------------------------------------------------- /frontend/src/features/products/ProductApi.jsx: -------------------------------------------------------------------------------- 1 | import { axiosi } from "../../config/axios"; 2 | 3 | export const addProduct=async(data)=>{ 4 | try { 5 | const res=await axiosi.post('/products',data) 6 | return res.data 7 | } catch (error) { 8 | throw error.response.data 9 | } 10 | } 11 | export const fetchProducts=async(filters)=>{ 12 | 13 | let queryString='' 14 | 15 | if(filters.brand){ 16 | filters.brand.map((brand)=>{ 17 | queryString+=`brand=${brand}&` 18 | }) 19 | } 20 | if(filters.category){ 21 | filters.category.map((category)=>{ 22 | queryString+=`category=${category}&` 23 | }) 24 | } 25 | 26 | if(filters.pagination){ 27 | queryString+=`page=${filters.pagination.page}&limit=${filters.pagination.limit}&` 28 | } 29 | 30 | if(filters.sort){ 31 | queryString+=`sort=${filters.sort.sort}&order=${filters.sort.order}&` 32 | } 33 | 34 | if(filters.user){ 35 | queryString+=`user=${filters.user}&` 36 | } 37 | 38 | try { 39 | const res=await axiosi.get(`/products?${queryString}`) 40 | const totalResults=await res.headers.get("X-Total-Count") 41 | return {data:res.data,totalResults:totalResults} 42 | } catch (error) { 43 | throw error.response.data 44 | } 45 | } 46 | export const fetchProductById=async(id)=>{ 47 | try { 48 | const res=await axiosi.get(`/products/${id}`) 49 | return res.data 50 | } catch (error) { 51 | throw error.response.data 52 | } 53 | } 54 | export const updateProductById=async(update)=>{ 55 | try { 56 | const res=await axiosi.patch(`/products/${update._id}`,update) 57 | return res.data 58 | } catch (error) { 59 | throw error.response.data 60 | } 61 | } 62 | export const undeleteProductById=async(id)=>{ 63 | try { 64 | const res=await axiosi.patch(`/products/undelete/${id}`) 65 | return res.data 66 | } catch (error) { 67 | throw error.response.data 68 | } 69 | } 70 | export const deleteProductById=async(id)=>{ 71 | try { 72 | const res=await axiosi.delete(`/products/${id}`) 73 | return res.data 74 | } catch (error) { 75 | throw error.response.data 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /frontend/src/features/products/ProductSlice.jsx: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 2 | import { addProduct, deleteProductById, fetchProductById, fetchProducts, undeleteProductById, updateProductById } from "./ProductApi"; 3 | 4 | 5 | const initialState={ 6 | status:"idle", 7 | productUpdateStatus:'idle', 8 | productAddStatus:"idle", 9 | productFetchStatus:"idle", 10 | products:[], 11 | totalResults:0, 12 | isFilterOpen:false, 13 | selectedProduct:null, 14 | errors:null, 15 | successMessage:null 16 | } 17 | 18 | export const addProductAsync=createAsyncThunk("products/addProductAsync",async(data)=>{ 19 | const addedProduct=await addProduct(data) 20 | return addedProduct 21 | }) 22 | export const fetchProductsAsync=createAsyncThunk("products/fetchProductsAsync",async(filters)=>{ 23 | const products=await fetchProducts(filters) 24 | return products 25 | }) 26 | export const fetchProductByIdAsync=createAsyncThunk("products/fetchProductByIdAsync",async(id)=>{ 27 | const selectedProduct=await fetchProductById(id) 28 | return selectedProduct 29 | }) 30 | export const updateProductByIdAsync=createAsyncThunk("products/updateProductByIdAsync",async(update)=>{ 31 | const updatedProduct=await updateProductById(update) 32 | return updatedProduct 33 | }) 34 | export const undeleteProductByIdAsync=createAsyncThunk("products/undeleteProductByIdAsync",async(id)=>{ 35 | const unDeletedProduct=await undeleteProductById(id) 36 | return unDeletedProduct 37 | }) 38 | export const deleteProductByIdAsync=createAsyncThunk("products/deleteProductByIdAsync",async(id)=>{ 39 | const deletedProduct=await deleteProductById(id) 40 | return deletedProduct 41 | }) 42 | 43 | const productSlice=createSlice({ 44 | name:"productSlice", 45 | initialState:initialState, 46 | reducers:{ 47 | clearProductErrors:(state)=>{ 48 | state.errors=null 49 | }, 50 | clearProductSuccessMessage:(state)=>{ 51 | state.successMessage=null 52 | }, 53 | resetProductStatus:(state)=>{ 54 | state.status='idle' 55 | }, 56 | clearSelectedProduct:(state)=>{ 57 | state.selectedProduct=null 58 | }, 59 | resetProductUpdateStatus:(state)=>{ 60 | state.productUpdateStatus='idle' 61 | }, 62 | resetProductAddStatus:(state)=>{ 63 | state.productAddStatus='idle' 64 | }, 65 | toggleFilters:(state)=>{ 66 | state.isFilterOpen=!state.isFilterOpen 67 | }, 68 | resetProductFetchStatus:(state)=>{ 69 | state.productFetchStatus='idle' 70 | } 71 | }, 72 | extraReducers:(builder)=>{ 73 | builder 74 | .addCase(addProductAsync.pending,(state)=>{ 75 | state.productAddStatus='pending' 76 | }) 77 | .addCase(addProductAsync.fulfilled,(state,action)=>{ 78 | state.productAddStatus='fullfilled' 79 | state.products.push(action.payload) 80 | }) 81 | .addCase(addProductAsync.rejected,(state,action)=>{ 82 | state.productAddStatus='rejected' 83 | state.errors=action.error 84 | }) 85 | 86 | .addCase(fetchProductsAsync.pending,(state)=>{ 87 | state.productFetchStatus='pending' 88 | }) 89 | .addCase(fetchProductsAsync.fulfilled,(state,action)=>{ 90 | state.productFetchStatus='fullfilled' 91 | state.products=action.payload.data 92 | state.totalResults=action.payload.totalResults 93 | }) 94 | .addCase(fetchProductsAsync.rejected,(state,action)=>{ 95 | state.productFetchStatus='rejected' 96 | state.errors=action.error 97 | }) 98 | 99 | .addCase(fetchProductByIdAsync.pending,(state)=>{ 100 | state.productFetchStatus='pending' 101 | }) 102 | .addCase(fetchProductByIdAsync.fulfilled,(state,action)=>{ 103 | state.productFetchStatus='fullfilled' 104 | state.selectedProduct=action.payload 105 | }) 106 | .addCase(fetchProductByIdAsync.rejected,(state,action)=>{ 107 | state.productFetchStatus='rejected' 108 | state.errors=action.error 109 | }) 110 | 111 | .addCase(updateProductByIdAsync.pending,(state)=>{ 112 | state.productUpdateStatus='pending' 113 | }) 114 | .addCase(updateProductByIdAsync.fulfilled,(state,action)=>{ 115 | state.productUpdateStatus='fullfilled' 116 | const index=state.products.findIndex((product)=>product._id===action.payload._id) 117 | state.products[index]=action.payload 118 | }) 119 | .addCase(updateProductByIdAsync.rejected,(state,action)=>{ 120 | state.productUpdateStatus='rejected' 121 | state.errors=action.error 122 | }) 123 | 124 | .addCase(undeleteProductByIdAsync.pending,(state)=>{ 125 | state.status='pending' 126 | }) 127 | .addCase(undeleteProductByIdAsync.fulfilled,(state,action)=>{ 128 | state.status='fullfilled' 129 | const index=state.products.findIndex((product)=>product._id===action.payload._id) 130 | state.products[index]=action.payload 131 | }) 132 | .addCase(undeleteProductByIdAsync.rejected,(state,action)=>{ 133 | state.status='rejected' 134 | state.errors=action.error 135 | }) 136 | 137 | .addCase(deleteProductByIdAsync.pending,(state)=>{ 138 | state.status='pending' 139 | }) 140 | .addCase(deleteProductByIdAsync.fulfilled,(state,action)=>{ 141 | state.status='fullfilled' 142 | const index=state.products.findIndex((product)=>product._id===action.payload._id) 143 | state.products[index]=action.payload 144 | }) 145 | .addCase(deleteProductByIdAsync.rejected,(state,action)=>{ 146 | state.status='rejected' 147 | state.errors=action.error 148 | }) 149 | } 150 | }) 151 | 152 | // exporting selectors 153 | export const selectProductStatus=(state)=>state.ProductSlice.status 154 | export const selectProducts=(state)=>state.ProductSlice.products 155 | export const selectProductTotalResults=(state)=>state.ProductSlice.totalResults 156 | export const selectSelectedProduct=(state)=>state.ProductSlice.selectedProduct 157 | export const selectProductErrors=(state)=>state.ProductSlice.errors 158 | export const selectProductSuccessMessage=(state)=>state.ProductSlice.successMessage 159 | export const selectProductUpdateStatus=(state)=>state.ProductSlice.productUpdateStatus 160 | export const selectProductAddStatus=(state)=>state.ProductSlice.productAddStatus 161 | export const selectProductIsFilterOpen=(state)=>state.ProductSlice.isFilterOpen 162 | export const selectProductFetchStatus=(state)=>state.ProductSlice.productFetchStatus 163 | 164 | // exporting actions 165 | export const {clearProductSuccessMessage,clearProductErrors,clearSelectedProduct,resetProductStatus,resetProductUpdateStatus,resetProductAddStatus,toggleFilters,resetProductFetchStatus}=productSlice.actions 166 | 167 | export default productSlice.reducer 168 | -------------------------------------------------------------------------------- /frontend/src/features/products/components/ProductBanner.jsx: -------------------------------------------------------------------------------- 1 | import SwipeableViews from 'react-swipeable-views'; 2 | import { autoPlay } from 'react-swipeable-views-utils'; 3 | import MobileStepper from '@mui/material/MobileStepper'; 4 | import { Box, useTheme } from '@mui/material'; 5 | import { useState } from 'react'; 6 | 7 | const AutoPlaySwipeableViews = autoPlay(SwipeableViews); 8 | 9 | export const ProductBanner = ({images}) => { 10 | 11 | const theme=useTheme() 12 | 13 | const [activeStep, setActiveStep] = useState(0); 14 | const maxSteps = images.length; 15 | 16 | const handleNext = () => { 17 | setActiveStep((prevActiveStep) => prevActiveStep + 1); 18 | }; 19 | 20 | const handleBack = () => { 21 | setActiveStep((prevActiveStep) => prevActiveStep - 1); 22 | }; 23 | 24 | const handleStepChange = (step) => { 25 | setActiveStep(step); 26 | }; 27 | 28 | return ( 29 | <> 30 | 31 | { 32 | images.map((image,index) => ( 33 |
34 | { 35 | Math.abs(activeStep - index) <= 2 36 | ? 37 | 38 | : 39 | null 40 | } 41 |
42 | )) 43 | } 44 |
45 |
46 | 47 |
48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/features/products/components/ProductCard.jsx: -------------------------------------------------------------------------------- 1 | import { FormHelperText, Paper, Stack, Typography, useMediaQuery, useTheme} from '@mui/material' 2 | import React, { useState } from 'react' 3 | import { useNavigate } from 'react-router-dom' 4 | import FavoriteBorder from '@mui/icons-material/FavoriteBorder'; 5 | import Favorite from '@mui/icons-material/Favorite'; 6 | import Checkbox from '@mui/material/Checkbox'; 7 | import { useDispatch, useSelector } from 'react-redux'; 8 | import { selectWishlistItems } from '../../wishlist/WishlistSlice'; 9 | import { selectLoggedInUser } from '../../auth/AuthSlice'; 10 | import { addToCartAsync,selectCartItems } from '../../cart/CartSlice'; 11 | import {motion} from 'framer-motion' 12 | 13 | export const ProductCard = ({id,title,price,thumbnail,brand,stockQuantity,handleAddRemoveFromWishlist,isWishlistCard,isAdminCard}) => { 14 | 15 | 16 | const navigate=useNavigate() 17 | const wishlistItems=useSelector(selectWishlistItems) 18 | const loggedInUser=useSelector(selectLoggedInUser) 19 | const cartItems=useSelector(selectCartItems) 20 | const dispatch=useDispatch() 21 | let isProductAlreadyinWishlist=-1 22 | 23 | 24 | const theme=useTheme() 25 | const is1410=useMediaQuery(theme.breakpoints.down(1410)) 26 | const is932=useMediaQuery(theme.breakpoints.down(932)) 27 | const is752=useMediaQuery(theme.breakpoints.down(752)) 28 | const is500=useMediaQuery(theme.breakpoints.down(500)) 29 | const is608=useMediaQuery(theme.breakpoints.down(608)) 30 | const is488=useMediaQuery(theme.breakpoints.down(488)) 31 | const is408=useMediaQuery(theme.breakpoints.down(408)) 32 | 33 | isProductAlreadyinWishlist=wishlistItems.some((item)=>item.product._id===id) 34 | 35 | const isProductAlreadyInCart=cartItems.some((item)=>item.product._id===id) 36 | 37 | const handleAddToCart=async(e)=>{ 38 | e.stopPropagation() 39 | const data={user:loggedInUser?._id,product:id} 40 | dispatch(addToCartAsync(data)) 41 | } 42 | 43 | 44 | return ( 45 | <> 46 | 47 | { 48 | 49 | isProductAlreadyinWishlist!==-1 ? 50 | navigate(`/product-details/${id}`)}> 51 | 52 | {/* image display */} 53 | 54 | {`${title} 55 | 56 | 57 | {/* lower section */} 58 | 59 | 60 | 61 | 62 | {title} 63 | { 64 | !isAdminCard && 65 | 66 | e.stopPropagation()} checked={isProductAlreadyinWishlist} onChange={(e)=>handleAddRemoveFromWishlist(e,id)} icon={} checkedIcon={} /> 67 | 68 | } 69 | 70 | {brand} 71 | 72 | 73 | 74 | ${price} 75 | { 76 | !isWishlistCard? isProductAlreadyInCart? 77 | 'Added to cart' 78 | : 79 | !isAdminCard && 80 | handleAddToCart(e)} style={{padding:"10px 15px",borderRadius:"3px",outline:"none",border:"none",cursor:"pointer",backgroundColor:"black",color:"white",fontSize:is408?'.9rem':is488?'.7rem':is500?'.8rem':'.9rem'}}> 81 |
82 |

Add To Cart

83 |
84 |
85 | :'' 86 | } 87 | 88 |
89 | { 90 | stockQuantity<=20 && ( 91 | {stockQuantity===1?"Only 1 stock is left":"Only few are left"} 92 | ) 93 | } 94 |
95 |
96 | :'' 97 | 98 | 99 | } 100 | 101 | 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /frontend/src/features/review/ReviewApi.jsx: -------------------------------------------------------------------------------- 1 | import {axiosi} from '../../config/axios' 2 | 3 | export const createReview=async(review)=>{ 4 | try { 5 | const res=await axiosi.post('/reviews',review) 6 | return res.data 7 | } catch (error) { 8 | throw error.response.data 9 | } 10 | } 11 | export const fetchReviewsByProductId=async(id)=>{ 12 | try { 13 | const res=await axiosi.get(`/reviews/product/${id}`) 14 | return res.data 15 | } catch (error) { 16 | throw error.response.data 17 | } 18 | } 19 | 20 | export const updateReviewById=async(update)=>{ 21 | try { 22 | const res=await axiosi.patch(`/reviews/${update._id}`,update) 23 | return res.data 24 | } catch (error) { 25 | throw error.response.data 26 | } 27 | } 28 | export const deleteReviewById=async(id)=>{ 29 | try { 30 | const res=await axiosi.delete(`/reviews/${id}`) 31 | return res.data 32 | } catch (error) { 33 | throw error.response.data 34 | } 35 | } -------------------------------------------------------------------------------- /frontend/src/features/review/ReviewSlice.jsx: -------------------------------------------------------------------------------- 1 | import {createAsyncThunk, createSlice} from '@reduxjs/toolkit' 2 | import { createReview, deleteReviewById, fetchReviewsByProductId, updateReviewById } from './ReviewApi' 3 | 4 | 5 | const initialState={ 6 | status:"idle", 7 | reviewAddStatus:"idle", 8 | reviewDeleteStatus:"idle", 9 | reviewUpdateStatus:"idle", 10 | reviewFetchStatus:"idle", 11 | reviews:[], 12 | errors:null, 13 | successMessage:null 14 | } 15 | 16 | export const createReviewAsync=createAsyncThunk('review/createReviewAsync',async(review)=>{ 17 | const createdReview=await createReview(review) 18 | return createdReview 19 | }) 20 | 21 | export const fetchReviewsByProductIdAsync=createAsyncThunk('review/fetchReviewsByProductIdAsync',async(id)=>{ 22 | const reviews=await fetchReviewsByProductId(id) 23 | return reviews 24 | }) 25 | 26 | export const updateReviewByIdAsync=createAsyncThunk("review/updateReviewByIdAsync",async(update)=>{ 27 | const updatedReview=await updateReviewById(update) 28 | return updatedReview 29 | }) 30 | 31 | export const deleteReviewByIdAsync=createAsyncThunk('reviews/deleteReviewByIdAsync',async(id)=>{ 32 | const deletedReview=await deleteReviewById(id) 33 | return deletedReview 34 | }) 35 | 36 | const reviewSlice=createSlice({ 37 | name:"reviewSlice", 38 | initialState:initialState, 39 | reducers:{ 40 | resetReviewAddStatus:(state)=>{ 41 | state.reviewAddStatus='idle' 42 | }, 43 | resetReviewDeleteStatus:(state)=>{ 44 | state.reviewDeleteStatus='idle' 45 | }, 46 | resetReviewUpdateStatus:(state)=>{ 47 | state.reviewUpdateStatus='idle' 48 | }, 49 | resetReviewFetchStatus:(state)=>{ 50 | state.reviewFetchStatus='idle' 51 | } 52 | }, 53 | extraReducers:(builder)=>{ 54 | builder 55 | .addCase(createReviewAsync.pending,(state)=>{ 56 | state.reviewAddStatus='pending' 57 | }) 58 | .addCase(createReviewAsync.fulfilled,(state,action)=>{ 59 | state.reviewAddStatus='fulfilled' 60 | state.reviews.push(action.payload) 61 | }) 62 | .addCase(createReviewAsync.rejected,(state,action)=>{ 63 | state.reviewAddStatus='rejected' 64 | state.errors=action.error 65 | }) 66 | 67 | .addCase(fetchReviewsByProductIdAsync.pending,(state)=>{ 68 | state.reviewFetchStatus='pending' 69 | }) 70 | .addCase(fetchReviewsByProductIdAsync.fulfilled,(state,action)=>{ 71 | state.reviewFetchStatus='fulfilled' 72 | state.reviews=action.payload 73 | }) 74 | .addCase(fetchReviewsByProductIdAsync.rejected,(state,action)=>{ 75 | state.reviewFetchStatus='rejected' 76 | state.errors=action.error 77 | }) 78 | 79 | .addCase(updateReviewByIdAsync.pending,(state)=>{ 80 | state.reviewUpdateStatus='pending' 81 | }) 82 | .addCase(updateReviewByIdAsync.fulfilled,(state,action)=>{ 83 | state.reviewUpdateStatus='fulfilled' 84 | const index=state.reviews.findIndex((review)=>review._id===action.payload._id) 85 | state.reviews[index]=action.payload 86 | }) 87 | .addCase(updateReviewByIdAsync.rejected,(state,action)=>{ 88 | state.reviewUpdateStatus='rejected' 89 | state.errors=action.error 90 | }) 91 | 92 | .addCase(deleteReviewByIdAsync.pending,(state)=>{ 93 | state.reviewDeleteStatus='pending' 94 | }) 95 | .addCase(deleteReviewByIdAsync.fulfilled,(state,action)=>{ 96 | state.reviewDeleteStatus='fulfilled' 97 | state.reviews=state.reviews.filter((review)=>review._id!==action.payload._id) 98 | }) 99 | .addCase(deleteReviewByIdAsync.rejected,(state,action)=>{ 100 | state.reviewDeleteStatus='rejected' 101 | state.errors=action.error 102 | }) 103 | } 104 | }) 105 | 106 | 107 | // exporting selectors 108 | export const selectReviewStatus=(state)=>state.ReviewSlice.status 109 | export const selectReviews=(state)=>state.ReviewSlice.reviews 110 | export const selectReviewErrors=(state)=>state.ReviewSlice.errors 111 | export const selectReviewSuccessMessage=(state)=>state.ReviewSlice.successMessage 112 | export const selectReviewAddStatus=(state)=>state.ReviewSlice.reviewAddStatus 113 | export const selectReviewDeleteStatus=(state)=>state.ReviewSlice.reviewDeleteStatus 114 | export const selectReviewUpdateStatus=(state)=>state.ReviewSlice.reviewUpdateStatus 115 | export const selectReviewFetchStatus=(state)=>state.ReviewSlice.reviewFetchStatus 116 | 117 | // exporting actions 118 | export const {resetReviewAddStatus,resetReviewDeleteStatus,resetReviewUpdateStatus,resetReviewFetchStatus}=reviewSlice.actions 119 | 120 | export default reviewSlice.reducer -------------------------------------------------------------------------------- /frontend/src/features/review/components/ReviewItem.jsx: -------------------------------------------------------------------------------- 1 | import {Button, IconButton, Menu, MenuItem, Paper, Rating, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material' 2 | import React, { useEffect, useState } from 'react' 3 | import MoreVertIcon from '@mui/icons-material/MoreVert'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { selectLoggedInUser } from '../../auth/AuthSlice'; 6 | import {deleteReviewByIdAsync, selectReviewStatus, updateReviewByIdAsync} from '../ReviewSlice' 7 | import { useForm } from "react-hook-form" 8 | import {LoadingButton} from '@mui/lab' 9 | 10 | export const ReviewItem = ({id,username,userid,comment,rating,createdAt}) => { 11 | 12 | const dispatch=useDispatch() 13 | const loggedInUser=useSelector(selectLoggedInUser) 14 | const {register,handleSubmit,formState: { errors }} = useForm() 15 | const [edit,setEdit]=useState(false) 16 | const [editRating,setEditRating]=useState(rating) 17 | const theme=useTheme() 18 | const is480=useMediaQuery(theme.breakpoints.down(480)) 19 | 20 | const [anchorEl, setAnchorEl] = React.useState(null); 21 | const open = Boolean(anchorEl); 22 | const handleClick = (event) => { 23 | setAnchorEl(event.currentTarget); 24 | }; 25 | const handleClose = () => { 26 | setAnchorEl(null); 27 | }; 28 | 29 | const deleteReview=()=>{ 30 | dispatch(deleteReviewByIdAsync(id)) 31 | handleClose() 32 | } 33 | 34 | const handleUpdateReview=(data)=>{ 35 | const update={...data,_id:id,rating:editRating} 36 | dispatch(updateReviewByIdAsync(update)) 37 | setEdit(false) 38 | } 39 | 40 | const isOwnReview=userid===loggedInUser?._id 41 | 42 | return ( 43 | 44 | 45 | {/* user , rating and created at*/} 46 | 47 | 48 | 49 | 50 | {username} 51 | 52 | setEditRating(e.target.value)} value={edit?editRating:rating}/> 53 | 54 | 55 | 56 | 57 | { 58 | isOwnReview && ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {setEdit(true);handleClose()}}>Edit 67 | Delete 68 | 69 | 70 | ) 71 | } 72 | 73 | {new Date(createdAt).toDateString()} 74 | 75 | 76 | {/* review comment */} 77 | 78 | { 79 | edit?( 80 | 81 | 82 | 83 | Update 84 | 85 | 86 | 87 | ):({comment}) 88 | } 89 | 90 | 91 | 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /frontend/src/features/user/UserApi.jsx: -------------------------------------------------------------------------------- 1 | import { axiosi } from "../../config/axios" 2 | 3 | export const fetchLoggedInUserById=async(id)=>{ 4 | try { 5 | const res=await axiosi.get(`/users/${id}`) 6 | return res.data 7 | } catch (error) { 8 | throw error.response.data 9 | } 10 | } 11 | export const updateUserById=async(update)=>{ 12 | try { 13 | const res=await axiosi.patch(`/users/${update._id}`,update) 14 | return res.data 15 | } catch (error) { 16 | throw error.response.data 17 | } 18 | } -------------------------------------------------------------------------------- /frontend/src/features/user/UserSlice.jsx: -------------------------------------------------------------------------------- 1 | import {createAsyncThunk, createSlice} from '@reduxjs/toolkit' 2 | import { fetchLoggedInUserById, updateUserById } from './UserApi' 3 | 4 | const initialState={ 5 | status:"idle", 6 | userInfo:null, 7 | errors:null, 8 | successMessage:null 9 | } 10 | 11 | export const fetchLoggedInUserByIdAsync=createAsyncThunk('user/fetchLoggedInUserByIdAsync',async(id)=>{ 12 | const userInfo=await fetchLoggedInUserById(id) 13 | return userInfo 14 | }) 15 | export const updateUserByIdAsync=createAsyncThunk('user/updateUserByIdAsync',async(update)=>{ 16 | const updatedUser=await updateUserById(update) 17 | return updatedUser 18 | }) 19 | 20 | const userSlice=createSlice({ 21 | name:"userSlice", 22 | initialState:initialState, 23 | reducers:{}, 24 | extraReducers:(builder)=>{ 25 | builder 26 | .addCase(fetchLoggedInUserByIdAsync.pending,(state)=>{ 27 | state.status='pending' 28 | }) 29 | .addCase(fetchLoggedInUserByIdAsync.fulfilled,(state,action)=>{ 30 | state.status='fulfilled' 31 | state.userInfo=action.payload 32 | }) 33 | .addCase(fetchLoggedInUserByIdAsync.rejected,(state,action)=>{ 34 | state.status='rejected' 35 | state.errors=action.error 36 | }) 37 | 38 | .addCase(updateUserByIdAsync.pending,(state)=>{ 39 | state.status='pending' 40 | }) 41 | .addCase(updateUserByIdAsync.fulfilled,(state,action)=>{ 42 | state.status='fulfilled' 43 | state.userInfo=action.payload 44 | }) 45 | .addCase(updateUserByIdAsync.rejected,(state,action)=>{ 46 | state.status='rejected' 47 | state.errors=action.error 48 | }) 49 | } 50 | }) 51 | 52 | // exporting selectors 53 | export const selectUserStatus=(state)=>state.UserSlice.status 54 | export const selectUserInfo=(state)=>state.UserSlice.userInfo 55 | export const selectUserErrors=(state)=>state.UserSlice.errors 56 | export const selectUserSuccessMessage=(state)=>state.UserSlice.successMessage 57 | 58 | 59 | export default userSlice.reducer -------------------------------------------------------------------------------- /frontend/src/features/wishlist/WishlistApi.jsx: -------------------------------------------------------------------------------- 1 | import {axiosi} from '../../config/axios' 2 | 3 | export const createWishlistItem=async(data)=>{ 4 | try { 5 | const res=await axiosi.post("/wishlist",data) 6 | return res.data 7 | } catch (error) { 8 | throw error.response.data 9 | } 10 | } 11 | 12 | export const fetchWishlistByUserId=async(id)=>{ 13 | try { 14 | const res=await axiosi.get(`/wishlist/user/${id}`) 15 | const totalResults=await res.headers.get("X-Total-Count") 16 | return {data:res.data,totalResults:totalResults} 17 | } catch (error) { 18 | throw error.response.data 19 | } 20 | } 21 | 22 | export const updateWishlistItemById=async(update)=>{ 23 | try { 24 | const res=await axiosi.patch(`/wishlist/${update._id}`,update) 25 | return res.data 26 | } catch (error) { 27 | throw error.response.data 28 | } 29 | } 30 | 31 | export const deleteWishlistItemById=async(id)=>{ 32 | try { 33 | const res=await axiosi.delete(`/wishlist/${id}`) 34 | return res.data 35 | } catch (error) { 36 | throw error.response.data 37 | } 38 | } -------------------------------------------------------------------------------- /frontend/src/features/wishlist/WishlistSlice.jsx: -------------------------------------------------------------------------------- 1 | import {createAsyncThunk, createSlice} from '@reduxjs/toolkit' 2 | import { createWishlistItem, deleteWishlistItemById, fetchWishlistByUserId, updateWishlistItemById } from './WishlistApi' 3 | 4 | const initialState={ 5 | wishlistItemUpdateStatus:"idle", 6 | wishlistItemAddStatus:'idle', 7 | wishlistItemDeleteStatus:"idle", 8 | wishlistFetchStatus:'idle', 9 | items:[], 10 | totalResults:0, 11 | errors:null, 12 | successMessage:null, 13 | } 14 | 15 | export const createWishlistItemAsync=createAsyncThunk('wishlist/createWishlistItemAsync',async(data)=>{ 16 | const createdItem=await createWishlistItem(data) 17 | return createdItem 18 | }) 19 | export const fetchWishlistByUserIdAsync=createAsyncThunk('wishlist/fetchWishlistByUserIdAsync',async(id)=>{ 20 | const fetchedWishlist=await fetchWishlistByUserId(id) 21 | return fetchedWishlist 22 | }) 23 | export const updateWishlistItemByIdAsync=createAsyncThunk('wishlist/updateWishlistItemByIdAsync',async(update)=>{ 24 | const updatedWishlistItem=await updateWishlistItemById(update) 25 | return updatedWishlistItem 26 | }) 27 | export const deleteWishlistItemByIdAsync=createAsyncThunk('wishlist/deleteWishlistItemByIdAsync',async(id)=>{ 28 | const deletedWishlistItem=await deleteWishlistItemById(id) 29 | return deletedWishlistItem 30 | }) 31 | 32 | const wishlistSlice=createSlice({ 33 | name:"wishlistSlice", 34 | initialState:initialState, 35 | reducers:{ 36 | resetWishlistItemUpdateStatus:(state)=>{ 37 | state.wishlistItemUpdateStatus='idle' 38 | }, 39 | resetWishlistItemAddStatus:(state)=>{ 40 | state.wishlistItemAddStatus='idle' 41 | }, 42 | resetWishlistItemDeleteStatus:(state)=>{ 43 | state.wishlistItemDeleteStatus='idle' 44 | }, 45 | resetWishlistFetchStatus:(state)=>{ 46 | state.wishlistFetchStatus='idle' 47 | }, 48 | 49 | }, 50 | extraReducers:(builder)=>{ 51 | builder 52 | .addCase(createWishlistItemAsync.pending,(state)=>{ 53 | state.wishlistItemAddStatus='pending' 54 | }) 55 | .addCase(createWishlistItemAsync.fulfilled,(state,action)=>{ 56 | state.wishlistItemAddStatus='fulfilled' 57 | state.items.push(action.payload) 58 | }) 59 | .addCase(createWishlistItemAsync.rejected,(state,action)=>{ 60 | state.wishlistItemAddStatus='rejected' 61 | state.errors=action.error 62 | }) 63 | 64 | .addCase(fetchWishlistByUserIdAsync.pending,(state)=>{ 65 | state.wishlistFetchStatus='pending' 66 | }) 67 | .addCase(fetchWishlistByUserIdAsync.fulfilled,(state,action)=>{ 68 | state.wishlistFetchStatus='fulfilled' 69 | state.items=action.payload.data 70 | state.totalResults=action.payload.totalResults 71 | }) 72 | .addCase(fetchWishlistByUserIdAsync.rejected,(state,action)=>{ 73 | state.wishlistFetchStatus='rejected' 74 | state.errors=action.error 75 | }) 76 | 77 | .addCase(updateWishlistItemByIdAsync.pending,(state)=>{ 78 | state.wishlistItemUpdateStatus='pending' 79 | }) 80 | .addCase(updateWishlistItemByIdAsync.fulfilled,(state,action)=>{ 81 | state.wishlistItemUpdateStatus='fulfilled' 82 | const index=state.items.findIndex((item)=>item._id===action.payload._id) 83 | state.items[index]=action.payload 84 | }) 85 | .addCase(updateWishlistItemByIdAsync.rejected,(state,action)=>{ 86 | state.wishlistItemUpdateStatus='rejected' 87 | state.errors=action.error 88 | }) 89 | 90 | .addCase(deleteWishlistItemByIdAsync.pending,(state)=>{ 91 | state.wishlistItemDeleteStatus='pending' 92 | }) 93 | .addCase(deleteWishlistItemByIdAsync.fulfilled,(state,action)=>{ 94 | state.wishlistItemDeleteStatus='fulfilled' 95 | state.items=state.items.filter((item)=>item._id!==action.payload._id) 96 | }) 97 | .addCase(deleteWishlistItemByIdAsync.rejected,(state,action)=>{ 98 | state.wishlistItemDeleteStatus='rejected' 99 | state.errors=action.error 100 | }) 101 | } 102 | }) 103 | 104 | 105 | // exporting selectors 106 | export const selectWishlistItems=(state)=>state.WishlistSlice.items 107 | export const selectWishlistFetchStatus=(state)=>state.WishlistSlice.wishlistFetchStatus 108 | export const selectWishlistItemUpdateStatus=(state)=>state.WishlistSlice.wishlistItemUpdateStatus 109 | export const selectWishlistItemAddStatus=(state)=>state.WishlistSlice.wishlistItemAddStatus 110 | export const selectWishlistItemDeleteStatus=(state)=>state.WishlistSlice.wishlistItemDeleteStatus 111 | export const selectWishlistErrors=(state)=>state.WishlistSlice.errors 112 | export const selectWishlistSuccessMessage=(state)=>state.WishlistSlice.successMessage 113 | export const selectWishlistTotalResults=(state)=>state.WishlistSlice.totalResults 114 | 115 | // exporting actions 116 | export const {resetWishlistFetchStatus,resetWishlistItemAddStatus,resetWishlistItemDeleteStatus,resetWishlistItemUpdateStatus}=wishlistSlice.actions 117 | 118 | export default wishlistSlice.reducer -------------------------------------------------------------------------------- /frontend/src/hooks/useAuth/useAuthCheck.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { checkAuthAsync } from '../../features/auth/AuthSlice' 3 | import { useDispatch } from 'react-redux' 4 | 5 | export const useAuthCheck = () => { 6 | 7 | const dispatch = useDispatch(); 8 | 9 | useEffect(()=>{ 10 | dispatch(checkAuthAsync()) 11 | },[dispatch]) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAuth/useFetchLoggedInUserDetails.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { selectLoggedInUser } from '../../features/auth/AuthSlice' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { fetchAddressByUserIdAsync } from '../../features/address/AddressSlice' 5 | import { fetchWishlistByUserIdAsync } from '../../features/wishlist/WishlistSlice' 6 | import { fetchCartByUserIdAsync } from '../../features/cart/CartSlice' 7 | import { fetchAllCategoriesAsync } from '../../features/categories/CategoriesSlice' 8 | import { fetchAllBrandsAsync } from '../../features/brands/BrandSlice' 9 | import { fetchLoggedInUserByIdAsync } from '../../features/user/UserSlice' 10 | 11 | export const useFetchLoggedInUserDetails = (deps) => { 12 | 13 | const loggedInUser=useSelector(selectLoggedInUser) 14 | const dispatch = useDispatch(); 15 | 16 | useEffect(()=>{ 17 | /* when a user is logged in then this dispatches an action to get all the details of loggedInUser, 18 | as while login and signup only the bare-minimum information is sent by the server */ 19 | if(deps && loggedInUser?.isVerified){ 20 | dispatch(fetchLoggedInUserByIdAsync(loggedInUser?._id)) 21 | dispatch(fetchAllBrandsAsync()) 22 | dispatch(fetchAllCategoriesAsync()) 23 | 24 | if(!loggedInUser.isAdmin){ 25 | dispatch(fetchCartByUserIdAsync(loggedInUser?._id)) 26 | dispatch(fetchAddressByUserIdAsync(loggedInUser?._id)) 27 | dispatch(fetchWishlistByUserIdAsync(loggedInUser?._id)) 28 | } 29 | } 30 | },[deps]) 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import {Provider} from 'react-redux' 5 | import { store } from './app/store'; 6 | import {ToastContainer} from 'react-toastify' 7 | import 'react-toastify/dist/ReactToastify.css'; 8 | import { ThemeProvider } from '@mui/material'; 9 | import theme from './theme/theme'; 10 | 11 | const root = ReactDOM.createRoot(document.getElementById('root')); 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | -------------------------------------------------------------------------------- /frontend/src/layout/RootLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Outlet } from 'react-router-dom' 3 | 4 | export const RootLayout = () => { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/pages/AddProductPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Navbar } from '../features/navigation/components/Navbar' 3 | import { AddProduct } from '../features/admin/components/AddProduct' 4 | 5 | export const AddProductPage = () => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/pages/AdminDashboardPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Navbar } from '../features/navigation/components/Navbar' 3 | import { AdminDashBoard } from '../features/admin/components/AdminDashBoard' 4 | 5 | export const AdminDashboardPage = () => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/pages/AdminOrdersPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AdminOrders } from '../features/admin/components/AdminOrders' 3 | import {Navbar} from '../features/navigation/components/Navbar' 4 | 5 | export const AdminOrdersPage = () => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/pages/CartPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Navbar } from '../features/navigation/components/Navbar' 3 | import { Cart } from '../features/cart/components/Cart' 4 | import {Footer} from '../features/footer/Footer' 5 | 6 | export const CartPage = () => { 7 | return ( 8 | <> 9 | 10 | 11 |