├── .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 |
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 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | {/* 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 |
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 |
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 |
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 |
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 |
12 | >
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/pages/CheckoutPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Checkout } from '../features/checkout/components/Checkout'
3 | import {Footer} from '../features/footer/Footer'
4 |
5 | export const CheckoutPage = () => {
6 | return (
7 | <>
8 |
9 |
10 | >
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/pages/ForgotPasswordPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ForgotPassword } from '../features/auth/components/ForgotPassword'
3 |
4 | export const ForgotPasswordPage = () => {
5 | return (
6 |
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/pages/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Navbar } from '../features/navigation/components/Navbar'
3 | import { ProductList } from '../features/products/components/ProductList'
4 | import { resetAddressStatus, selectAddressStatus } from '../features/address/AddressSlice'
5 | import { useDispatch, useSelector } from 'react-redux'
6 | import {Footer} from '../features/footer/Footer'
7 |
8 | export const HomePage = () => {
9 |
10 | const dispatch=useDispatch()
11 | const addressStatus=useSelector(selectAddressStatus)
12 |
13 | useEffect(()=>{
14 | if(addressStatus==='fulfilled'){
15 |
16 | dispatch(resetAddressStatus())
17 | }
18 | },[addressStatus])
19 |
20 | return (
21 | <>
22 |
23 |
24 |
25 | >
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/pages/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Login } from '../features/auth/components/Login'
3 |
4 | export const LoginPage = () => {
5 | return (
6 |
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/pages/NotFoundPage.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Paper, Stack, Typography } from '@mui/material'
2 | import React from 'react'
3 | import { Link } from 'react-router-dom'
4 | import { notFoundPageAnimation } from '../assets'
5 | import Lottie from 'lottie-react'
6 |
7 |
8 | export const NotFoundPage = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 404 Not Found
20 | Sorry, we coudn't find the page you were looking for
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/pages/OrderSuccessPage.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Paper, Stack, Typography, useMediaQuery, useTheme } from '@mui/material'
2 | import React, { useEffect } from 'react'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { Link, useNavigate, useParams } from 'react-router-dom'
5 | import { resetCurrentOrder, selectCurrentOrder } from '../features/order/OrderSlice'
6 | import { selectUserInfo } from '../features/user/UserSlice'
7 | import { orderSuccessAnimation } from '../assets'
8 | import Lottie from 'lottie-react'
9 |
10 | export const OrderSuccessPage = () => {
11 |
12 |
13 | const navigate=useNavigate()
14 | const dispatch=useDispatch()
15 | const currentOrder=useSelector(selectCurrentOrder)
16 | const userDetails=useSelector(selectUserInfo)
17 | const {id}=useParams()
18 |
19 | const theme=useTheme()
20 | const is480=useMediaQuery(theme.breakpoints.down(480))
21 |
22 | useEffect(()=>{
23 | if(!currentOrder){
24 | navigate("/")
25 | }
26 | },[currentOrder])
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Hey {userDetails?.name}
39 | Your Order #{currentOrder?._id} is confirmed
40 | Thankyou for shopping with us❤️
41 |
42 |
43 |
44 |
45 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/src/pages/OtpVerificationPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { OtpVerfication } from '../features/auth/components/OtpVerfication'
3 |
4 | export const OtpVerificationPage = () => {
5 | return (
6 |
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/pages/ProductDetailsPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Navbar } from '../features/navigation/components/Navbar'
3 | import { ProductDetails } from '../features/products/components/ProductDetails'
4 | import { Footer } from '../features/footer/Footer'
5 |
6 | export const ProductDetailsPage = () => {
7 | return (
8 | <>
9 |
10 |
11 |
12 | >
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/pages/ProductUpdatePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ProductUpdate } from '../features/admin/components/ProductUpdate'
3 | import {Navbar} from '../features/navigation/components/Navbar'
4 |
5 | export const ProductUpdatePage = () => {
6 | return (
7 | <>
8 |
9 |
10 | >
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/pages/ResetPasswordPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ResetPassword } from '../features/auth/components/ResetPassword'
3 |
4 | export const ResetPasswordPage = () => {
5 | return (
6 |
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/pages/SignupPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Signup } from '../features/auth/components/Signup'
3 |
4 | export const SignupPage = () => {
5 | return (
6 |
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/pages/UserOrdersPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { UserOrders } from '../features/order/components/UserOrders'
3 | import {Navbar} from '../features/navigation/components/Navbar'
4 | import {Footer} from '../features/footer/Footer'
5 |
6 | export const UserOrdersPage = () => {
7 | return (
8 | <>
9 |
10 |
11 |
12 | >
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/pages/UserProfilePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { UserProfile } from '../features/user/components/UserProfile'
3 | import {Navbar} from '../features/navigation/components/Navbar'
4 |
5 | export const UserProfilePage = () => {
6 | return (
7 | <>
8 |
9 |
10 | >
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/pages/WishlistPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Wishlist } from '../features/wishlist/components/Wishlist'
3 | import {Navbar} from '../features/navigation/components/Navbar'
4 | import { Footer } from '../features/footer/Footer'
5 |
6 | export const WishlistPage = () => {
7 | return (
8 | <>
9 |
10 |
11 |
12 | >
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import { AddProductPage } from "./AddProductPage";
2 | import { AdminOrdersPage } from "./AdminOrdersPage";
3 | import { CartPage } from "./CartPage";
4 | import { CheckoutPage } from "./CheckoutPage";
5 | import { ForgotPasswordPage } from "./ForgotPasswordPage";
6 | import { HomePage } from "./HomePage";
7 | import { LoginPage } from "./LoginPage";
8 | import { OrderSuccessPage } from "./OrderSuccessPage";
9 | import { OtpVerificationPage } from "./OtpVerificationPage";
10 | import { ProductDetailsPage } from "./ProductDetailsPage";
11 | import { ProductUpdatePage } from "./ProductUpdatePage";
12 | import { ResetPasswordPage } from "./ResetPasswordPage";
13 | import { SignupPage } from "./SignupPage";
14 | import { UserOrdersPage } from "./UserOrdersPage";
15 | import { UserProfilePage } from "./UserProfilePage";
16 | import { WishlistPage } from "./WishlistPage";
17 |
18 | export {
19 | SignupPage,
20 | LoginPage,
21 | ForgotPasswordPage,
22 | ResetPasswordPage,
23 | HomePage,
24 | ProductDetailsPage,
25 | CartPage,
26 | UserProfilePage,
27 | CheckoutPage,
28 | OrderSuccessPage,
29 | UserOrdersPage,
30 | ProductUpdatePage,
31 | AddProductPage,
32 | AdminOrdersPage,
33 | WishlistPage,
34 | OtpVerificationPage
35 | }
--------------------------------------------------------------------------------
/frontend/src/theme/theme.js:
--------------------------------------------------------------------------------
1 | import { createTheme } from "@mui/material/styles";
2 |
3 | export const theme = createTheme({
4 | palette: {
5 | primary: {
6 | main:"#000000",
7 | light:"#ffffff",
8 | dark:'#DB4444',
9 | customBlack:"#191919"
10 | },
11 | secondary:{
12 | main:"#background.paper"
13 | },
14 | },
15 |
16 |
17 |
18 | breakpoints: {
19 | values: {
20 | xs: 0,
21 | sm: 600,
22 | md: 900,
23 | lg: 1200,
24 | xl: 1536,
25 | },
26 | },
27 | typography: {
28 | fontFamily:"Poppins, sans-serif",
29 | h1: { // -1rem rule
30 | fontSize: "6rem",
31 |
32 | "@media (max-width:960px)": {
33 | fontSize: "5rem",
34 | },
35 | "@media (max-width:600px)": {
36 | fontSize: "4rem",
37 | },
38 | "@media (max-width:414px)": {
39 | fontSize: "2.5rem",
40 | },
41 | },
42 | h2: { // -7 formula
43 | fontSize: "3.75rem",
44 | "@media (max-width:960px)": {
45 | fontSize: "3rem",
46 | },
47 |
48 | "@media (max-width:662px)": {
49 | fontSize: "2.3rem",
50 | },
51 | "@media (max-width:414px)": {
52 | fontSize: "2.2rem",
53 | },
54 | },
55 | h3: { // -6 formula
56 | fontSize: "3rem",
57 | "@media (max-width:960px)": {
58 | fontSize: "2.4rem",
59 | },
60 |
61 | "@media (max-width:662px)": {
62 | fontSize: "2rem",
63 | },
64 | "@media (max-width:414px)": {
65 | fontSize: "1.7rem",
66 | },
67 |
68 | },
69 | h4: {
70 | fontSize: "2.125rem",
71 | "@media (max-width:960px)": {
72 | fontSize: "1.5rem",
73 | },
74 | "@media (max-width:600px)": {
75 | fontSize: "1.25rem",
76 | },
77 | },
78 | h5: {
79 | fontSize: "1.5rem",
80 | "@media (max-width:960px)": {
81 | fontSize: "1.25rem",
82 | },
83 | "@media (max-width:600px)": {
84 | fontSize: "1.1rem",
85 | },
86 | },
87 | h6: {
88 | fontSize: "1.25rem",
89 | "@media (max-width:960px)": {
90 | fontSize: "1.1rem",
91 | },
92 | "@media (max-width:600px)": {
93 | fontSize: "1rem",
94 | },
95 | },
96 | body1: {
97 | fontSize: "1rem",
98 | "@media (max-width:960px)": {
99 | fontSize: "1rem",
100 | },
101 | "@media (max-width:600px)": {
102 | fontSize: ".9rem",
103 | },
104 | },
105 | body2: {
106 | fontSize: "1rem",
107 | "@media (max-width:960px)": {
108 | fontSize: "1rem",
109 | },
110 | "@media (max-width:600px)": {
111 | fontSize: "1rem",
112 | },
113 | "@media (max-width:480px)": {
114 | fontSize: ".97rem",
115 | },
116 | },
117 | },
118 | });
119 |
120 | export default theme;
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-ecommerce",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {}
6 | }
7 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## MERN Ecommerce: A Seamless Shopping Experience Powered by the MERN Stack, Redux Toolkit, and Material UI
2 |
3 | ### Also try -> [https://mernchat.in](https://mernchat.in)
4 | ### ```Note✨: I have another amazing project on``` [end-to-end-encrypted-chat-application](https://github.com/RishiBakshii/mern-chat) ```using Next.js, Prisma, Postgresql, Express, Socket.io.```
5 |
6 | **MERN Ecommerce** is a full-stack application designed to transform your online shopping experience. Built with the MERN stack (MongoDB, Express.js, React, Node.js), it leverages Redux Toolkit for efficient state management and Material UI for a sleek, user-friendly interface. This project offers a robust platform for both users and admins, packed with essential features for a seamless experience.
7 |
8 | 
9 |
10 | 
11 |
12 |
13 | # **Features**
14 |
15 | ### **User:**
16 | - **Product Reviews:**
17 | - Write, edit, and delete reviews.
18 | - Instant updates on ratings and star percentages.
19 |
20 | - **Wishlist:**
21 | - Add, remove, and annotate products with personalized notes.
22 |
23 | - **Order Management:**
24 | - Create new orders and view order history.
25 |
26 | - **Profile Management:**
27 | - Manage email, username, and multiple addresses.
28 |
29 | - **Shopping Cart:**
30 | - Add products, adjust quantities, and view subtotals.
31 |
32 | ### **Admin:**
33 | - **Product Management:**
34 | - Add, edit, delete, and soft-delete products.
35 | - Manage product attributes like name and stock.
36 |
37 | - **Order Management:**
38 | - View and update order details and status.
39 |
40 | ### **Security & User Experience:**
41 | - **Secure Authentication:**
42 | - Login, signup, OTP verification, password reset, and logout.
43 |
44 | - **Intuitive Interface:**
45 | - Powered by Material UI for a visually appealing and user-friendly experience.
46 |
47 | ### **Scalability:**
48 | - **Built for Growth:**
49 | - Scalable architecture to handle increasing user demands.
50 |
51 |
52 | # **Project Setup**
53 |
54 | ### Prerequisites
55 | - Node.js ( version v21.1.0 or later )
56 | - MongoDB installed and running locally
57 |
58 | ### Clone the project
59 |
60 | ```bash
61 | git clone https://github.com/RishiBakshii/mern-ecommerce.git
62 | ```
63 |
64 | ### Navigate to the project directory
65 |
66 | ```bash
67 | cd mern-ecommerce
68 | ```
69 |
70 | ### Install dependencies for frontend and backend separately
71 | **Tip:** To efficiently install dependencies for both frontend and backend simultaneously, use split terminals.
72 |
73 | Install frontend dependencies
74 | ```bash
75 | cd frontend
76 | npm install
77 | ```
78 |
79 | Install backend dependencies
80 |
81 | ```bash
82 | cd backend
83 | npm install
84 | ```
85 |
86 |
87 | ### Environment Variables
88 | **Backend**
89 | - Create a `.env` file in the `backend` directory.
90 | - Add the following variables with appropriate values
91 | ```bash
92 | # Database connection string
93 | MONGO_URI="mongodb://localhost:27017/your-database-name"
94 |
95 | # Frontend URL (adjust if needed)
96 | ORIGIN="http://localhost:3000"
97 |
98 | # Email credentials for sending password resets and OTPs
99 | EMAIL="your-email@example.com"
100 | PASSWORD="your-email-password"
101 |
102 | # Token and cookie expiration settings
103 | LOGIN_TOKEN_EXPIRATION="30d" # Days
104 | OTP_EXPIRATION_TIME="120000" # Milliseconds
105 | PASSWORD_RESET_TOKEN_EXPIRATION="2m" # Minutes
106 | COOKIE_EXPIRATION_DAYS="30" # Days
107 |
108 | # Secret key for jwt security
109 | SECRET_KEY="your-secret-key"
110 |
111 | # Environment (production/development)
112 | PRODUCTION="false" # Initially set to false for development
113 | ```
114 |
115 | **Frontend**
116 | - Create a `.env` file in the `frontend` directory
117 | - Add the following variable:
118 | ```bash
119 | # Backend URL (adjust if needed)
120 | REACT_APP_BASE_URL="http://localhost:8000"
121 | ```
122 |
123 | **Important**
124 | - Replace all placeholders (e.g., your_database_name, your_email) with your actual values.
125 | - Exclude the `.env` file from version control to protect sensitive information.
126 |
127 | ### Data seeding
128 | - **Get started quickly with pre-populated data**: Populate your database with sample users, products, reviews, and carts, enabling you to test functionalities without manual data entry.
129 |
130 | **Steps**:
131 | - Open a new terminal window.
132 | - Navigate to the `backend` directory: `cd backend`
133 | - Run the seeding script: `npm run seed` ( This script executes the `seed.js` file within the `seed` subdirectory equivalent to running `node seed/seed.js` )
134 | ### Running Development Servers
135 |
136 | **Important:**
137 |
138 | - **Separate terminals**: Run the commands in separate terminal windows or use `split terminal` to avoid conflicts.
139 | - **Nodemon required**: Ensure you have `nodemon` installed globally to run the backend development servers using `npm run dev`. You can install it globally using `npm install -g nodemon`.
140 |
141 | #### Start the backend server
142 | - Navigate to the `backend` directory: `cd backend`
143 | - Start the server: `npm run dev` (or npm start)
144 | - You should see a message indicating the server is running, usually on port 8000.
145 |
146 | #### Start the frontend server:
147 | - Navigate to the `frontend` directory: `cd frontend`
148 | - Start the server: `npm start`
149 | - You should see a message indicating the server is running, usually on port 3000.
150 |
151 | ### Login with demo account (Optional)
152 | - After successfully seeding the database, you can now explore the application's functionalities using pre-populated sample data.
153 | - here are the `login credentials`
154 | ```bash
155 | email: demo@gmail.com
156 | pass: helloWorld@123
157 | ```
158 |
159 | - **Please Note**: While the demo account provides a convenient way to explore many features, it has some limitations:
160 | - **Password Reset and OTP Verification**: Due to security reasons, the demo account uses a non-real email address. Therefore, password reset and OTP verification functionalities are not available for this account.
161 |
162 | **What this means**:
163 | - You cannot request a password reset or receive verification codes on the demo email address.
164 | - To test password reset and OTP verification flows, you need to create a genuine account with a valid email address.
165 |
166 | **What to do?**
167 | - If you're primarily interested in exploring other functionalities like wishlist, cart, and order history, the demo account is sufficient.
168 | - To test password reset and OTP verification, create a personal account with a valid email address.
169 | ### Accessing the Application
170 | Once both servers are running, you can access them at the following URL's:
171 | - Backend: http://localhost:8000
172 | - Frontend: http://localhost:3000
173 |
174 | ## **Bonus**
175 | Don't forget to star the repository and share your feedback!✨
176 |
177 | ## Authors
178 | - [@RishiBakshii](https://github.com/RishiBakshii)
179 |
--------------------------------------------------------------------------------