├── .gitignore ├── LICENSE ├── README.md ├── backend ├── controllers │ ├── analytics.controller.js │ ├── auth.controller.js │ ├── cart.controller.js │ ├── coupon.controller.js │ ├── payment.controller.js │ └── product.controller.js ├── lib │ ├── cloudinary.js │ ├── db.js │ ├── redis.js │ └── stripe.js ├── middleware │ └── auth.middleware.js ├── models │ ├── coupon.model.js │ ├── order.model.js │ ├── product.model.js │ └── user.model.js ├── routes │ ├── analytics.route.js │ ├── auth.route.js │ ├── cart.route.js │ ├── coupon.route.js │ ├── payment.route.js │ └── product.route.js └── server.js ├── frontend ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── bags.jpg │ ├── glasses.png │ ├── jackets.jpg │ ├── jeans.jpg │ ├── screenshot-for-readme.png │ ├── shoes.jpg │ ├── suits.jpg │ ├── tshirts.jpg │ └── vite.svg ├── src │ ├── App.jsx │ ├── components │ │ ├── AnalyticsTab.jsx │ │ ├── CartItem.jsx │ │ ├── CategoryItem.jsx │ │ ├── CreateProductForm.jsx │ │ ├── FeaturedProducts.jsx │ │ ├── GiftCouponCard.jsx │ │ ├── LoadingSpinner.jsx │ │ ├── Navbar.jsx │ │ ├── OrderSummary.jsx │ │ ├── PeopleAlsoBought.jsx │ │ ├── ProductCard.jsx │ │ └── ProductsList.jsx │ ├── index.css │ ├── lib │ │ └── axios.js │ ├── main.jsx │ ├── pages │ │ ├── AdminPage.jsx │ │ ├── CartPage.jsx │ │ ├── CategoryPage.jsx │ │ ├── HomePage.jsx │ │ ├── LoginPage.jsx │ │ ├── PurchaseCancelPage.jsx │ │ ├── PurchaseSuccessPage.jsx │ │ └── SignUpPage.jsx │ └── stores │ │ ├── useCartStore.js │ │ ├── useProductStore.js │ │ └── useUserStore.js ├── tailwind.config.js └── vite.config.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | .env 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Burak 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

E-Commerce Store 🛒

2 | 3 | ![Demo App](/frontend/public/screenshot-for-readme.png) 4 | 5 | [Video Tutorial on Youtube](https://youtu.be/sX57TLIPNx8) 6 | 7 | About This Course: 8 | 9 | - 🚀 Project Setup 10 | - 🗄️ MongoDB & Redis Integration 11 | - 💳 Stripe Payment Setup 12 | - 🔐 Robust Authentication System 13 | - 🔑 JWT with Refresh/Access Tokens 14 | - 📝 User Signup & Login 15 | - 🛒 E-Commerce Core 16 | - 📦 Product & Category Management 17 | - 🛍️ Shopping Cart Functionality 18 | - 💰 Checkout with Stripe 19 | - 🏷️ Coupon Code System 20 | - 👑 Admin Dashboard 21 | - 📊 Sales Analytics 22 | - 🎨 Design with Tailwind 23 | - 🛒 Cart & Checkout Process 24 | - 🔒 Security 25 | - 🛡️ Data Protection 26 | - 🚀Caching with Redis 27 | - ⌛ And a lot more... 28 | 29 | ### Setup .env file 30 | 31 | ```bash 32 | PORT=5000 33 | MONGO_URI=your_mongo_uri 34 | 35 | UPSTASH_REDIS_URL=your_redis_url 36 | 37 | ACCESS_TOKEN_SECRET=your_access_token_secret 38 | REFRESH_TOKEN_SECRET=your_refresh_token_secret 39 | 40 | CLOUDINARY_CLOUD_NAME=your_cloud_name 41 | CLOUDINARY_API_KEY=your_api_key 42 | CLOUDINARY_API_SECRET=your_api_secret 43 | 44 | STRIPE_SECRET_KEY=your_stripe_secret_key 45 | CLIENT_URL=http://localhost:5173 46 | NODE_ENV=development 47 | ``` 48 | 49 | ### Run this app locally 50 | 51 | ```shell 52 | npm run build 53 | ``` 54 | 55 | ### Start the app 56 | 57 | ```shell 58 | npm run start 59 | ``` 60 | -------------------------------------------------------------------------------- /backend/controllers/analytics.controller.js: -------------------------------------------------------------------------------- 1 | import Order from "../models/order.model.js"; 2 | import Product from "../models/product.model.js"; 3 | import User from "../models/user.model.js"; 4 | 5 | export const getAnalyticsData = async () => { 6 | const totalUsers = await User.countDocuments(); 7 | const totalProducts = await Product.countDocuments(); 8 | 9 | const salesData = await Order.aggregate([ 10 | { 11 | $group: { 12 | _id: null, // it groups all documents together, 13 | totalSales: { $sum: 1 }, 14 | totalRevenue: { $sum: "$totalAmount" }, 15 | }, 16 | }, 17 | ]); 18 | 19 | const { totalSales, totalRevenue } = salesData[0] || { totalSales: 0, totalRevenue: 0 }; 20 | 21 | return { 22 | users: totalUsers, 23 | products: totalProducts, 24 | totalSales, 25 | totalRevenue, 26 | }; 27 | }; 28 | 29 | export const getDailySalesData = async (startDate, endDate) => { 30 | try { 31 | const dailySalesData = await Order.aggregate([ 32 | { 33 | $match: { 34 | createdAt: { 35 | $gte: startDate, 36 | $lte: endDate, 37 | }, 38 | }, 39 | }, 40 | { 41 | $group: { 42 | _id: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } }, 43 | sales: { $sum: 1 }, 44 | revenue: { $sum: "$totalAmount" }, 45 | }, 46 | }, 47 | { $sort: { _id: 1 } }, 48 | ]); 49 | 50 | // example of dailySalesData 51 | // [ 52 | // { 53 | // _id: "2024-08-18", 54 | // sales: 12, 55 | // revenue: 1450.75 56 | // }, 57 | // ] 58 | 59 | const dateArray = getDatesInRange(startDate, endDate); 60 | // console.log(dateArray) // ['2024-08-18', '2024-08-19', ... ] 61 | 62 | return dateArray.map((date) => { 63 | const foundData = dailySalesData.find((item) => item._id === date); 64 | 65 | return { 66 | date, 67 | sales: foundData?.sales || 0, 68 | revenue: foundData?.revenue || 0, 69 | }; 70 | }); 71 | } catch (error) { 72 | throw error; 73 | } 74 | }; 75 | 76 | function getDatesInRange(startDate, endDate) { 77 | const dates = []; 78 | let currentDate = new Date(startDate); 79 | 80 | while (currentDate <= endDate) { 81 | dates.push(currentDate.toISOString().split("T")[0]); 82 | currentDate.setDate(currentDate.getDate() + 1); 83 | } 84 | 85 | return dates; 86 | } 87 | -------------------------------------------------------------------------------- /backend/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | import { redis } from "../lib/redis.js"; 2 | import User from "../models/user.model.js"; 3 | import jwt from "jsonwebtoken"; 4 | 5 | const generateTokens = (userId) => { 6 | const accessToken = jwt.sign({ userId }, process.env.ACCESS_TOKEN_SECRET, { 7 | expiresIn: "15m", 8 | }); 9 | 10 | const refreshToken = jwt.sign({ userId }, process.env.REFRESH_TOKEN_SECRET, { 11 | expiresIn: "7d", 12 | }); 13 | 14 | return { accessToken, refreshToken }; 15 | }; 16 | 17 | const storeRefreshToken = async (userId, refreshToken) => { 18 | await redis.set(`refresh_token:${userId}`, refreshToken, "EX", 7 * 24 * 60 * 60); // 7days 19 | }; 20 | 21 | const setCookies = (res, accessToken, refreshToken) => { 22 | res.cookie("accessToken", accessToken, { 23 | httpOnly: true, // prevent XSS attacks, cross site scripting attack 24 | secure: process.env.NODE_ENV === "production", 25 | sameSite: "strict", // prevents CSRF attack, cross-site request forgery attack 26 | maxAge: 15 * 60 * 1000, // 15 minutes 27 | }); 28 | res.cookie("refreshToken", refreshToken, { 29 | httpOnly: true, // prevent XSS attacks, cross site scripting attack 30 | secure: process.env.NODE_ENV === "production", 31 | sameSite: "strict", // prevents CSRF attack, cross-site request forgery attack 32 | maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days 33 | }); 34 | }; 35 | 36 | export const signup = async (req, res) => { 37 | const { email, password, name } = req.body; 38 | try { 39 | const userExists = await User.findOne({ email }); 40 | 41 | if (userExists) { 42 | return res.status(400).json({ message: "User already exists" }); 43 | } 44 | const user = await User.create({ name, email, password }); 45 | 46 | // authenticate 47 | const { accessToken, refreshToken } = generateTokens(user._id); 48 | await storeRefreshToken(user._id, refreshToken); 49 | 50 | setCookies(res, accessToken, refreshToken); 51 | 52 | res.status(201).json({ 53 | _id: user._id, 54 | name: user.name, 55 | email: user.email, 56 | role: user.role, 57 | }); 58 | } catch (error) { 59 | console.log("Error in signup controller", error.message); 60 | res.status(500).json({ message: error.message }); 61 | } 62 | }; 63 | 64 | export const login = async (req, res) => { 65 | try { 66 | const { email, password } = req.body; 67 | const user = await User.findOne({ email }); 68 | 69 | if (user && (await user.comparePassword(password))) { 70 | const { accessToken, refreshToken } = generateTokens(user._id); 71 | await storeRefreshToken(user._id, refreshToken); 72 | setCookies(res, accessToken, refreshToken); 73 | 74 | res.json({ 75 | _id: user._id, 76 | name: user.name, 77 | email: user.email, 78 | role: user.role, 79 | }); 80 | } else { 81 | res.status(400).json({ message: "Invalid email or password" }); 82 | } 83 | } catch (error) { 84 | console.log("Error in login controller", error.message); 85 | res.status(500).json({ message: error.message }); 86 | } 87 | }; 88 | 89 | export const logout = async (req, res) => { 90 | try { 91 | const refreshToken = req.cookies.refreshToken; 92 | if (refreshToken) { 93 | const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET); 94 | await redis.del(`refresh_token:${decoded.userId}`); 95 | } 96 | 97 | res.clearCookie("accessToken"); 98 | res.clearCookie("refreshToken"); 99 | res.json({ message: "Logged out successfully" }); 100 | } catch (error) { 101 | console.log("Error in logout controller", error.message); 102 | res.status(500).json({ message: "Server error", error: error.message }); 103 | } 104 | }; 105 | 106 | // this will refresh the access token 107 | export const refreshToken = async (req, res) => { 108 | try { 109 | const refreshToken = req.cookies.refreshToken; 110 | 111 | if (!refreshToken) { 112 | return res.status(401).json({ message: "No refresh token provided" }); 113 | } 114 | 115 | const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET); 116 | const storedToken = await redis.get(`refresh_token:${decoded.userId}`); 117 | 118 | if (storedToken !== refreshToken) { 119 | return res.status(401).json({ message: "Invalid refresh token" }); 120 | } 121 | 122 | const accessToken = jwt.sign({ userId: decoded.userId }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "15m" }); 123 | 124 | res.cookie("accessToken", accessToken, { 125 | httpOnly: true, 126 | secure: process.env.NODE_ENV === "production", 127 | sameSite: "strict", 128 | maxAge: 15 * 60 * 1000, 129 | }); 130 | 131 | res.json({ message: "Token refreshed successfully" }); 132 | } catch (error) { 133 | console.log("Error in refreshToken controller", error.message); 134 | res.status(500).json({ message: "Server error", error: error.message }); 135 | } 136 | }; 137 | 138 | export const getProfile = async (req, res) => { 139 | try { 140 | res.json(req.user); 141 | } catch (error) { 142 | res.status(500).json({ message: "Server error", error: error.message }); 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /backend/controllers/cart.controller.js: -------------------------------------------------------------------------------- 1 | import Product from "../models/product.model.js"; 2 | 3 | export const getCartProducts = async (req, res) => { 4 | try { 5 | const products = await Product.find({ _id: { $in: req.user.cartItems } }); 6 | 7 | // add quantity for each product 8 | const cartItems = products.map((product) => { 9 | const item = req.user.cartItems.find((cartItem) => cartItem.id === product.id); 10 | return { ...product.toJSON(), quantity: item.quantity }; 11 | }); 12 | 13 | res.json(cartItems); 14 | } catch (error) { 15 | console.log("Error in getCartProducts controller", error.message); 16 | res.status(500).json({ message: "Server error", error: error.message }); 17 | } 18 | }; 19 | 20 | export const addToCart = async (req, res) => { 21 | try { 22 | const { productId } = req.body; 23 | const user = req.user; 24 | 25 | const existingItem = user.cartItems.find((item) => item.id === productId); 26 | if (existingItem) { 27 | existingItem.quantity += 1; 28 | } else { 29 | user.cartItems.push(productId); 30 | } 31 | 32 | await user.save(); 33 | res.json(user.cartItems); 34 | } catch (error) { 35 | console.log("Error in addToCart controller", error.message); 36 | res.status(500).json({ message: "Server error", error: error.message }); 37 | } 38 | }; 39 | 40 | export const removeAllFromCart = async (req, res) => { 41 | try { 42 | const { productId } = req.body; 43 | const user = req.user; 44 | if (!productId) { 45 | user.cartItems = []; 46 | } else { 47 | user.cartItems = user.cartItems.filter((item) => item.id !== productId); 48 | } 49 | await user.save(); 50 | res.json(user.cartItems); 51 | } catch (error) { 52 | res.status(500).json({ message: "Server error", error: error.message }); 53 | } 54 | }; 55 | 56 | export const updateQuantity = async (req, res) => { 57 | try { 58 | const { id: productId } = req.params; 59 | const { quantity } = req.body; 60 | const user = req.user; 61 | const existingItem = user.cartItems.find((item) => item.id === productId); 62 | 63 | if (existingItem) { 64 | if (quantity === 0) { 65 | user.cartItems = user.cartItems.filter((item) => item.id !== productId); 66 | await user.save(); 67 | return res.json(user.cartItems); 68 | } 69 | 70 | existingItem.quantity = quantity; 71 | await user.save(); 72 | res.json(user.cartItems); 73 | } else { 74 | res.status(404).json({ message: "Product not found" }); 75 | } 76 | } catch (error) { 77 | console.log("Error in updateQuantity controller", error.message); 78 | res.status(500).json({ message: "Server error", error: error.message }); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /backend/controllers/coupon.controller.js: -------------------------------------------------------------------------------- 1 | import Coupon from "../models/coupon.model.js"; 2 | 3 | export const getCoupon = async (req, res) => { 4 | try { 5 | const coupon = await Coupon.findOne({ userId: req.user._id, isActive: true }); 6 | res.json(coupon || null); 7 | } catch (error) { 8 | console.log("Error in getCoupon controller", error.message); 9 | res.status(500).json({ message: "Server error", error: error.message }); 10 | } 11 | }; 12 | 13 | export const validateCoupon = async (req, res) => { 14 | try { 15 | const { code } = req.body; 16 | const coupon = await Coupon.findOne({ code: code, userId: req.user._id, isActive: true }); 17 | 18 | if (!coupon) { 19 | return res.status(404).json({ message: "Coupon not found" }); 20 | } 21 | 22 | if (coupon.expirationDate < new Date()) { 23 | coupon.isActive = false; 24 | await coupon.save(); 25 | return res.status(404).json({ message: "Coupon expired" }); 26 | } 27 | 28 | res.json({ 29 | message: "Coupon is valid", 30 | code: coupon.code, 31 | discountPercentage: coupon.discountPercentage, 32 | }); 33 | } catch (error) { 34 | console.log("Error in validateCoupon controller", error.message); 35 | res.status(500).json({ message: "Server error", error: error.message }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /backend/controllers/payment.controller.js: -------------------------------------------------------------------------------- 1 | import Coupon from "../models/coupon.model.js"; 2 | import Order from "../models/order.model.js"; 3 | import { stripe } from "../lib/stripe.js"; 4 | 5 | export const createCheckoutSession = async (req, res) => { 6 | try { 7 | const { products, couponCode } = req.body; 8 | 9 | if (!Array.isArray(products) || products.length === 0) { 10 | return res.status(400).json({ error: "Invalid or empty products array" }); 11 | } 12 | 13 | let totalAmount = 0; 14 | 15 | const lineItems = products.map((product) => { 16 | const amount = Math.round(product.price * 100); // stripe wants u to send in the format of cents 17 | totalAmount += amount * product.quantity; 18 | 19 | return { 20 | price_data: { 21 | currency: "usd", 22 | product_data: { 23 | name: product.name, 24 | images: [product.image], 25 | }, 26 | unit_amount: amount, 27 | }, 28 | quantity: product.quantity || 1, 29 | }; 30 | }); 31 | 32 | let coupon = null; 33 | if (couponCode) { 34 | coupon = await Coupon.findOne({ code: couponCode, userId: req.user._id, isActive: true }); 35 | if (coupon) { 36 | totalAmount -= Math.round((totalAmount * coupon.discountPercentage) / 100); 37 | } 38 | } 39 | 40 | const session = await stripe.checkout.sessions.create({ 41 | payment_method_types: ["card"], 42 | line_items: lineItems, 43 | mode: "payment", 44 | success_url: `${process.env.CLIENT_URL}/purchase-success?session_id={CHECKOUT_SESSION_ID}`, 45 | cancel_url: `${process.env.CLIENT_URL}/purchase-cancel`, 46 | discounts: coupon 47 | ? [ 48 | { 49 | coupon: await createStripeCoupon(coupon.discountPercentage), 50 | }, 51 | ] 52 | : [], 53 | metadata: { 54 | userId: req.user._id.toString(), 55 | couponCode: couponCode || "", 56 | products: JSON.stringify( 57 | products.map((p) => ({ 58 | id: p._id, 59 | quantity: p.quantity, 60 | price: p.price, 61 | })) 62 | ), 63 | }, 64 | }); 65 | 66 | if (totalAmount >= 20000) { 67 | await createNewCoupon(req.user._id); 68 | } 69 | res.status(200).json({ id: session.id, totalAmount: totalAmount / 100 }); 70 | } catch (error) { 71 | console.error("Error processing checkout:", error); 72 | res.status(500).json({ message: "Error processing checkout", error: error.message }); 73 | } 74 | }; 75 | 76 | export const checkoutSuccess = async (req, res) => { 77 | try { 78 | const { sessionId } = req.body; 79 | const session = await stripe.checkout.sessions.retrieve(sessionId); 80 | 81 | if (session.payment_status === "paid") { 82 | if (session.metadata.couponCode) { 83 | await Coupon.findOneAndUpdate( 84 | { 85 | code: session.metadata.couponCode, 86 | userId: session.metadata.userId, 87 | }, 88 | { 89 | isActive: false, 90 | } 91 | ); 92 | } 93 | 94 | // create a new Order 95 | const products = JSON.parse(session.metadata.products); 96 | const newOrder = new Order({ 97 | user: session.metadata.userId, 98 | products: products.map((product) => ({ 99 | product: product.id, 100 | quantity: product.quantity, 101 | price: product.price, 102 | })), 103 | totalAmount: session.amount_total / 100, // convert from cents to dollars, 104 | stripeSessionId: sessionId, 105 | }); 106 | 107 | await newOrder.save(); 108 | 109 | res.status(200).json({ 110 | success: true, 111 | message: "Payment successful, order created, and coupon deactivated if used.", 112 | orderId: newOrder._id, 113 | }); 114 | } 115 | } catch (error) { 116 | console.error("Error processing successful checkout:", error); 117 | res.status(500).json({ message: "Error processing successful checkout", error: error.message }); 118 | } 119 | }; 120 | 121 | async function createStripeCoupon(discountPercentage) { 122 | const coupon = await stripe.coupons.create({ 123 | percent_off: discountPercentage, 124 | duration: "once", 125 | }); 126 | 127 | return coupon.id; 128 | } 129 | 130 | async function createNewCoupon(userId) { 131 | await Coupon.findOneAndDelete({ userId }); 132 | 133 | const newCoupon = new Coupon({ 134 | code: "GIFT" + Math.random().toString(36).substring(2, 8).toUpperCase(), 135 | discountPercentage: 10, 136 | expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days from now 137 | userId: userId, 138 | }); 139 | 140 | await newCoupon.save(); 141 | 142 | return newCoupon; 143 | } 144 | -------------------------------------------------------------------------------- /backend/controllers/product.controller.js: -------------------------------------------------------------------------------- 1 | import { redis } from "../lib/redis.js"; 2 | import cloudinary from "../lib/cloudinary.js"; 3 | import Product from "../models/product.model.js"; 4 | 5 | export const getAllProducts = async (req, res) => { 6 | try { 7 | const products = await Product.find({}); // find all products 8 | res.json({ products }); 9 | } catch (error) { 10 | console.log("Error in getAllProducts controller", error.message); 11 | res.status(500).json({ message: "Server error", error: error.message }); 12 | } 13 | }; 14 | 15 | export const getFeaturedProducts = async (req, res) => { 16 | try { 17 | let featuredProducts = await redis.get("featured_products"); 18 | if (featuredProducts) { 19 | return res.json(JSON.parse(featuredProducts)); 20 | } 21 | 22 | // if not in redis, fetch from mongodb 23 | // .lean() is gonna return a plain javascript object instead of a mongodb document 24 | // which is good for performance 25 | featuredProducts = await Product.find({ isFeatured: true }).lean(); 26 | 27 | if (!featuredProducts) { 28 | return res.status(404).json({ message: "No featured products found" }); 29 | } 30 | 31 | // store in redis for future quick access 32 | 33 | await redis.set("featured_products", JSON.stringify(featuredProducts)); 34 | 35 | res.json(featuredProducts); 36 | } catch (error) { 37 | console.log("Error in getFeaturedProducts controller", error.message); 38 | res.status(500).json({ message: "Server error", error: error.message }); 39 | } 40 | }; 41 | 42 | export const createProduct = async (req, res) => { 43 | try { 44 | const { name, description, price, image, category } = req.body; 45 | 46 | let cloudinaryResponse = null; 47 | 48 | if (image) { 49 | cloudinaryResponse = await cloudinary.uploader.upload(image, { folder: "products" }); 50 | } 51 | 52 | const product = await Product.create({ 53 | name, 54 | description, 55 | price, 56 | image: cloudinaryResponse?.secure_url ? cloudinaryResponse.secure_url : "", 57 | category, 58 | }); 59 | 60 | res.status(201).json(product); 61 | } catch (error) { 62 | console.log("Error in createProduct controller", error.message); 63 | res.status(500).json({ message: "Server error", error: error.message }); 64 | } 65 | }; 66 | 67 | export const deleteProduct = async (req, res) => { 68 | try { 69 | const product = await Product.findById(req.params.id); 70 | 71 | if (!product) { 72 | return res.status(404).json({ message: "Product not found" }); 73 | } 74 | 75 | if (product.image) { 76 | const publicId = product.image.split("/").pop().split(".")[0]; 77 | try { 78 | await cloudinary.uploader.destroy(`products/${publicId}`); 79 | console.log("deleted image from cloduinary"); 80 | } catch (error) { 81 | console.log("error deleting image from cloduinary", error); 82 | } 83 | } 84 | 85 | await Product.findByIdAndDelete(req.params.id); 86 | 87 | res.json({ message: "Product deleted successfully" }); 88 | } catch (error) { 89 | console.log("Error in deleteProduct controller", error.message); 90 | res.status(500).json({ message: "Server error", error: error.message }); 91 | } 92 | }; 93 | 94 | export const getRecommendedProducts = async (req, res) => { 95 | try { 96 | const products = await Product.aggregate([ 97 | { 98 | $sample: { size: 4 }, 99 | }, 100 | { 101 | $project: { 102 | _id: 1, 103 | name: 1, 104 | description: 1, 105 | image: 1, 106 | price: 1, 107 | }, 108 | }, 109 | ]); 110 | 111 | res.json(products); 112 | } catch (error) { 113 | console.log("Error in getRecommendedProducts controller", error.message); 114 | res.status(500).json({ message: "Server error", error: error.message }); 115 | } 116 | }; 117 | 118 | export const getProductsByCategory = async (req, res) => { 119 | const { category } = req.params; 120 | try { 121 | const products = await Product.find({ category }); 122 | res.json({ products }); 123 | } catch (error) { 124 | console.log("Error in getProductsByCategory controller", error.message); 125 | res.status(500).json({ message: "Server error", error: error.message }); 126 | } 127 | }; 128 | 129 | export const toggleFeaturedProduct = async (req, res) => { 130 | try { 131 | const product = await Product.findById(req.params.id); 132 | if (product) { 133 | product.isFeatured = !product.isFeatured; 134 | const updatedProduct = await product.save(); 135 | await updateFeaturedProductsCache(); 136 | res.json(updatedProduct); 137 | } else { 138 | res.status(404).json({ message: "Product not found" }); 139 | } 140 | } catch (error) { 141 | console.log("Error in toggleFeaturedProduct controller", error.message); 142 | res.status(500).json({ message: "Server error", error: error.message }); 143 | } 144 | }; 145 | 146 | async function updateFeaturedProductsCache() { 147 | try { 148 | // The lean() method is used to return plain JavaScript objects instead of full Mongoose documents. This can significantly improve performance 149 | 150 | const featuredProducts = await Product.find({ isFeatured: true }).lean(); 151 | await redis.set("featured_products", JSON.stringify(featuredProducts)); 152 | } catch (error) { 153 | console.log("error in update cache function"); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /backend/lib/cloudinary.js: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from "cloudinary"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | cloudinary.config({ 7 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 8 | api_key: process.env.CLOUDINARY_API_KEY, 9 | api_secret: process.env.CLOUDINARY_API_SECRET, 10 | }); 11 | 12 | export default cloudinary; 13 | -------------------------------------------------------------------------------- /backend/lib/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | export const connectDB = async () => { 4 | try { 5 | const conn = await mongoose.connect(process.env.MONGO_URI); 6 | console.log(`MongoDB connected: ${conn.connection.host}`); 7 | } catch (error) { 8 | console.log("Error connecting to MONGODB", error.message); 9 | process.exit(1); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /backend/lib/redis.js: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export const redis = new Redis(process.env.UPSTASH_REDIS_URL); 7 | -------------------------------------------------------------------------------- /backend/lib/stripe.js: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); 7 | -------------------------------------------------------------------------------- /backend/middleware/auth.middleware.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import User from "../models/user.model.js"; 3 | 4 | export const protectRoute = async (req, res, next) => { 5 | try { 6 | const accessToken = req.cookies.accessToken; 7 | 8 | if (!accessToken) { 9 | return res.status(401).json({ message: "Unauthorized - No access token provided" }); 10 | } 11 | 12 | try { 13 | const decoded = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET); 14 | const user = await User.findById(decoded.userId).select("-password"); 15 | 16 | if (!user) { 17 | return res.status(401).json({ message: "User not found" }); 18 | } 19 | 20 | req.user = user; 21 | 22 | next(); 23 | } catch (error) { 24 | if (error.name === "TokenExpiredError") { 25 | return res.status(401).json({ message: "Unauthorized - Access token expired" }); 26 | } 27 | throw error; 28 | } 29 | } catch (error) { 30 | console.log("Error in protectRoute middleware", error.message); 31 | return res.status(401).json({ message: "Unauthorized - Invalid access token" }); 32 | } 33 | }; 34 | 35 | export const adminRoute = (req, res, next) => { 36 | if (req.user && req.user.role === "admin") { 37 | next(); 38 | } else { 39 | return res.status(403).json({ message: "Access denied - Admin only" }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /backend/models/coupon.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const couponSchema = new mongoose.Schema( 4 | { 5 | code: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | }, 10 | discountPercentage: { 11 | type: Number, 12 | required: true, 13 | min: 0, 14 | max: 100, 15 | }, 16 | expirationDate: { 17 | type: Date, 18 | required: true, 19 | }, 20 | isActive: { 21 | type: Boolean, 22 | default: true, 23 | }, 24 | userId: { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: "User", 27 | required: true, 28 | unique: true, 29 | }, 30 | }, 31 | { 32 | timestamps: true, 33 | } 34 | ); 35 | 36 | const Coupon = mongoose.model("Coupon", couponSchema); 37 | 38 | export default Coupon; 39 | -------------------------------------------------------------------------------- /backend/models/order.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const orderSchema = new mongoose.Schema( 4 | { 5 | user: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: "User", 8 | required: true, 9 | }, 10 | products: [ 11 | { 12 | product: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: "Product", 15 | required: true, 16 | }, 17 | quantity: { 18 | type: Number, 19 | required: true, 20 | min: 1, 21 | }, 22 | price: { 23 | type: Number, 24 | required: true, 25 | min: 0, 26 | }, 27 | }, 28 | ], 29 | totalAmount: { 30 | type: Number, 31 | required: true, 32 | min: 0, 33 | }, 34 | stripeSessionId: { 35 | type: String, 36 | unique: true, 37 | }, 38 | }, 39 | { timestamps: true } 40 | ); 41 | 42 | const Order = mongoose.model("Order", orderSchema); 43 | 44 | export default Order; 45 | -------------------------------------------------------------------------------- /backend/models/product.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const productSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | description: { 10 | type: String, 11 | required: true, 12 | }, 13 | price: { 14 | type: Number, 15 | min: 0, 16 | required: true, 17 | }, 18 | image: { 19 | type: String, 20 | required: [true, "Image is required"], 21 | }, 22 | category: { 23 | type: String, 24 | required: true, 25 | }, 26 | isFeatured: { 27 | type: Boolean, 28 | default: false, 29 | }, 30 | }, 31 | { timestamps: true } 32 | ); 33 | 34 | const Product = mongoose.model("Product", productSchema); 35 | 36 | export default Product; 37 | -------------------------------------------------------------------------------- /backend/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import bcrypt from "bcryptjs"; 3 | 4 | const userSchema = new mongoose.Schema( 5 | { 6 | name: { 7 | type: String, 8 | required: [true, "Name is required"], 9 | }, 10 | email: { 11 | type: String, 12 | required: [true, "Email is required"], 13 | unique: true, 14 | lowercase: true, 15 | trim: true, 16 | }, 17 | password: { 18 | type: String, 19 | required: [true, "Password is required"], 20 | minlength: [6, "Password must be at least 6 characters long"], 21 | }, 22 | cartItems: [ 23 | { 24 | quantity: { 25 | type: Number, 26 | default: 1, 27 | }, 28 | product: { 29 | type: mongoose.Schema.Types.ObjectId, 30 | ref: "Product", 31 | }, 32 | }, 33 | ], 34 | role: { 35 | type: String, 36 | enum: ["customer", "admin"], 37 | default: "customer", 38 | }, 39 | }, 40 | { 41 | timestamps: true, 42 | } 43 | ); 44 | 45 | // Pre-save hook to hash password before saving to database 46 | userSchema.pre("save", async function (next) { 47 | if (!this.isModified("password")) return next(); 48 | 49 | try { 50 | const salt = await bcrypt.genSalt(10); 51 | this.password = await bcrypt.hash(this.password, salt); 52 | next(); 53 | } catch (error) { 54 | next(error); 55 | } 56 | }); 57 | 58 | userSchema.methods.comparePassword = async function (password) { 59 | return bcrypt.compare(password, this.password); 60 | }; 61 | 62 | const User = mongoose.model("User", userSchema); 63 | 64 | export default User; 65 | -------------------------------------------------------------------------------- /backend/routes/analytics.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { adminRoute, protectRoute } from "../middleware/auth.middleware.js"; 3 | import { getAnalyticsData, getDailySalesData } from "../controllers/analytics.controller.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.get("/", protectRoute, adminRoute, async (req, res) => { 8 | try { 9 | const analyticsData = await getAnalyticsData(); 10 | 11 | const endDate = new Date(); 12 | const startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000); 13 | 14 | const dailySalesData = await getDailySalesData(startDate, endDate); 15 | 16 | res.json({ 17 | analyticsData, 18 | dailySalesData, 19 | }); 20 | } catch (error) { 21 | console.log("Error in analytics route", error.message); 22 | res.status(500).json({ message: "Server error", error: error.message }); 23 | } 24 | }); 25 | 26 | export default router; 27 | -------------------------------------------------------------------------------- /backend/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { login, logout, signup, refreshToken, getProfile } from "../controllers/auth.controller.js"; 3 | import { protectRoute } from "../middleware/auth.middleware.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.post("/signup", signup); 8 | router.post("/login", login); 9 | router.post("/logout", logout); 10 | router.post("/refresh-token", refreshToken); 11 | router.get("/profile", protectRoute, getProfile); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /backend/routes/cart.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { addToCart, getCartProducts, removeAllFromCart, updateQuantity } from "../controllers/cart.controller.js"; 3 | import { protectRoute } from "../middleware/auth.middleware.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.get("/", protectRoute, getCartProducts); 8 | router.post("/", protectRoute, addToCart); 9 | router.delete("/", protectRoute, removeAllFromCart); 10 | router.put("/:id", protectRoute, updateQuantity); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /backend/routes/coupon.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { protectRoute } from "../middleware/auth.middleware.js"; 3 | import { getCoupon, validateCoupon } from "../controllers/coupon.controller.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.get("/", protectRoute, getCoupon); 8 | router.post("/validate", protectRoute, validateCoupon); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /backend/routes/payment.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { protectRoute } from "../middleware/auth.middleware.js"; 3 | import { checkoutSuccess, createCheckoutSession } from "../controllers/payment.controller.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.post("/create-checkout-session", protectRoute, createCheckoutSession); 8 | router.post("/checkout-success", protectRoute, checkoutSuccess); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /backend/routes/product.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | createProduct, 4 | deleteProduct, 5 | getAllProducts, 6 | getFeaturedProducts, 7 | getProductsByCategory, 8 | getRecommendedProducts, 9 | toggleFeaturedProduct, 10 | } from "../controllers/product.controller.js"; 11 | import { adminRoute, protectRoute } from "../middleware/auth.middleware.js"; 12 | 13 | const router = express.Router(); 14 | 15 | router.get("/", protectRoute, adminRoute, getAllProducts); 16 | router.get("/featured", getFeaturedProducts); 17 | router.get("/category/:category", getProductsByCategory); 18 | router.get("/recommendations", getRecommendedProducts); 19 | router.post("/", protectRoute, adminRoute, createProduct); 20 | router.patch("/:id", protectRoute, adminRoute, toggleFeaturedProduct); 21 | router.delete("/:id", protectRoute, adminRoute, deleteProduct); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import dotenv from "dotenv"; 3 | import cookieParser from "cookie-parser"; 4 | import path from "path"; 5 | 6 | import authRoutes from "./routes/auth.route.js"; 7 | import productRoutes from "./routes/product.route.js"; 8 | import cartRoutes from "./routes/cart.route.js"; 9 | import couponRoutes from "./routes/coupon.route.js"; 10 | import paymentRoutes from "./routes/payment.route.js"; 11 | import analyticsRoutes from "./routes/analytics.route.js"; 12 | 13 | import { connectDB } from "./lib/db.js"; 14 | 15 | dotenv.config(); 16 | 17 | const app = express(); 18 | const PORT = process.env.PORT || 5000; 19 | 20 | const __dirname = path.resolve(); 21 | 22 | app.use(express.json({ limit: "10mb" })); // allows you to parse the body of the request 23 | app.use(cookieParser()); 24 | 25 | app.use("/api/auth", authRoutes); 26 | app.use("/api/products", productRoutes); 27 | app.use("/api/cart", cartRoutes); 28 | app.use("/api/coupons", couponRoutes); 29 | app.use("/api/payments", paymentRoutes); 30 | app.use("/api/analytics", analyticsRoutes); 31 | 32 | if (process.env.NODE_ENV === "production") { 33 | app.use(express.static(path.join(__dirname, "/frontend/dist"))); 34 | 35 | app.get("*", (req, res) => { 36 | res.sendFile(path.resolve(__dirname, "frontend", "dist", "index.html")); 37 | }); 38 | } 39 | 40 | app.listen(PORT, () => { 41 | console.log("Server is running on http://localhost:" + PORT); 42 | connectDB(); 43 | }); 44 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import react from "eslint-plugin-react"; 4 | import reactHooks from "eslint-plugin-react-hooks"; 5 | import reactRefresh from "eslint-plugin-react-refresh"; 6 | 7 | export default [ 8 | { ignores: ["dist"] }, 9 | { 10 | files: ["**/*.{js,jsx}"], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: "latest", 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: "module", 18 | }, 19 | }, 20 | settings: { react: { version: "18.3" } }, 21 | plugins: { 22 | react, 23 | "react-hooks": reactHooks, 24 | "react-refresh": reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs["jsx-runtime"].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | "react/jsx-no-target-blank": "off", 32 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], 33 | "react/prop-types": "off", 34 | }, 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1", 15 | "lucide-react": "^0.435.0", 16 | "react-confetti": "^6.1.0", 17 | "react-hot-toast": "^2.4.1", 18 | "react-router-dom": "^6.26.1", 19 | "recharts": "^2.12.7", 20 | "framer-motion": "^11.3.30", 21 | "zustand": "^4.5.5", 22 | "@stripe/stripe-js": "^4.3.0", 23 | "axios": "^1.7.5" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.9.0", 27 | "@types/react": "^18.3.3", 28 | "@types/react-dom": "^18.3.0", 29 | "@vitejs/plugin-react": "^4.3.1", 30 | "autoprefixer": "^10.4.20", 31 | "eslint": "^9.9.0", 32 | "eslint-plugin-react": "^7.35.0", 33 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 34 | "eslint-plugin-react-refresh": "^0.4.9", 35 | "globals": "^15.9.0", 36 | "postcss": "^8.4.41", 37 | "tailwindcss": "^3.4.10", 38 | "vite": "^5.4.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/bags.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/bags.jpg -------------------------------------------------------------------------------- /frontend/public/glasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/glasses.png -------------------------------------------------------------------------------- /frontend/public/jackets.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/jackets.jpg -------------------------------------------------------------------------------- /frontend/public/jeans.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/jeans.jpg -------------------------------------------------------------------------------- /frontend/public/screenshot-for-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/screenshot-for-readme.png -------------------------------------------------------------------------------- /frontend/public/shoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/shoes.jpg -------------------------------------------------------------------------------- /frontend/public/suits.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/suits.jpg -------------------------------------------------------------------------------- /frontend/public/tshirts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/tshirts.jpg -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Route, Routes } from "react-router-dom"; 2 | 3 | import HomePage from "./pages/HomePage"; 4 | import SignUpPage from "./pages/SignUpPage"; 5 | import LoginPage from "./pages/LoginPage"; 6 | import AdminPage from "./pages/AdminPage"; 7 | import CategoryPage from "./pages/CategoryPage"; 8 | 9 | import Navbar from "./components/Navbar"; 10 | import { Toaster } from "react-hot-toast"; 11 | import { useUserStore } from "./stores/useUserStore"; 12 | import { useEffect } from "react"; 13 | import LoadingSpinner from "./components/LoadingSpinner"; 14 | import CartPage from "./pages/CartPage"; 15 | import { useCartStore } from "./stores/useCartStore"; 16 | import PurchaseSuccessPage from "./pages/PurchaseSuccessPage"; 17 | import PurchaseCancelPage from "./pages/PurchaseCancelPage"; 18 | 19 | function App() { 20 | const { user, checkAuth, checkingAuth } = useUserStore(); 21 | const { getCartItems } = useCartStore(); 22 | useEffect(() => { 23 | checkAuth(); 24 | }, [checkAuth]); 25 | 26 | useEffect(() => { 27 | if (!user) return; 28 | 29 | getCartItems(); 30 | }, [getCartItems, user]); 31 | 32 | if (checkingAuth) return ; 33 | 34 | return ( 35 |
36 | {/* Background gradient */} 37 |
38 |
39 |
40 |
41 |
42 | 43 |
44 | 45 | 46 | } /> 47 | : } /> 48 | : } /> 49 | : } 52 | /> 53 | } /> 54 | : } /> 55 | : } 58 | /> 59 | : } /> 60 | 61 |
62 | 63 |
64 | ); 65 | } 66 | 67 | export default App; 68 | -------------------------------------------------------------------------------- /frontend/src/components/AnalyticsTab.jsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { useEffect, useState } from "react"; 3 | import axios from "../lib/axios"; 4 | import { Users, Package, ShoppingCart, DollarSign } from "lucide-react"; 5 | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; 6 | 7 | const AnalyticsTab = () => { 8 | const [analyticsData, setAnalyticsData] = useState({ 9 | users: 0, 10 | products: 0, 11 | totalSales: 0, 12 | totalRevenue: 0, 13 | }); 14 | const [isLoading, setIsLoading] = useState(true); 15 | const [dailySalesData, setDailySalesData] = useState([]); 16 | 17 | useEffect(() => { 18 | const fetchAnalyticsData = async () => { 19 | try { 20 | const response = await axios.get("/analytics"); 21 | setAnalyticsData(response.data.analyticsData); 22 | setDailySalesData(response.data.dailySalesData); 23 | } catch (error) { 24 | console.error("Error fetching analytics data:", error); 25 | } finally { 26 | setIsLoading(false); 27 | } 28 | }; 29 | 30 | fetchAnalyticsData(); 31 | }, []); 32 | 33 | if (isLoading) { 34 | return
Loading...
; 35 | } 36 | 37 | return ( 38 |
39 |
40 | 46 | 52 | 58 | 64 |
65 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 87 | 95 | 96 | 97 | 98 |
99 | ); 100 | }; 101 | export default AnalyticsTab; 102 | 103 | const AnalyticsCard = ({ title, value, icon: Icon, color }) => ( 104 | 110 |
111 |
112 |

{title}

113 |

{value}

114 |
115 |
116 |
117 |
118 | 119 |
120 | 121 | ); 122 | -------------------------------------------------------------------------------- /frontend/src/components/CartItem.jsx: -------------------------------------------------------------------------------- 1 | import { Minus, Plus, Trash } from "lucide-react"; 2 | import { useCartStore } from "../stores/useCartStore"; 3 | 4 | const CartItem = ({ item }) => { 5 | const { removeFromCart, updateQuantity } = useCartStore(); 6 | 7 | return ( 8 |
9 |
10 |
11 | 12 |
13 | 14 | 15 |
16 |
17 | 25 |

{item.quantity}

26 | 34 |
35 | 36 |
37 |

${item.price}

38 |
39 |
40 | 41 |
42 |

43 | {item.name} 44 |

45 |

{item.description}

46 | 47 |
48 | 55 |
56 |
57 |
58 |
59 | ); 60 | }; 61 | export default CartItem; 62 | -------------------------------------------------------------------------------- /frontend/src/components/CategoryItem.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const CategoryItem = ({ category }) => { 4 | return ( 5 |
6 | 7 |
8 |
9 | {category.name} 15 |
16 |

{category.name}

17 |

Explore {category.name}

18 |
19 |
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default CategoryItem; 26 | -------------------------------------------------------------------------------- /frontend/src/components/CreateProductForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { motion } from "framer-motion"; 3 | import { PlusCircle, Upload, Loader } from "lucide-react"; 4 | import { useProductStore } from "../stores/useProductStore"; 5 | 6 | const categories = ["jeans", "t-shirts", "shoes", "glasses", "jackets", "suits", "bags"]; 7 | 8 | const CreateProductForm = () => { 9 | const [newProduct, setNewProduct] = useState({ 10 | name: "", 11 | description: "", 12 | price: "", 13 | category: "", 14 | image: "", 15 | }); 16 | 17 | const { createProduct, loading } = useProductStore(); 18 | 19 | const handleSubmit = async (e) => { 20 | e.preventDefault(); 21 | try { 22 | await createProduct(newProduct); 23 | setNewProduct({ name: "", description: "", price: "", category: "", image: "" }); 24 | } catch { 25 | console.log("error creating a product"); 26 | } 27 | }; 28 | 29 | const handleImageChange = (e) => { 30 | const file = e.target.files[0]; 31 | if (file) { 32 | const reader = new FileReader(); 33 | 34 | reader.onloadend = () => { 35 | setNewProduct({ ...newProduct, image: reader.result }); 36 | }; 37 | 38 | reader.readAsDataURL(file); // base64 39 | } 40 | }; 41 | 42 | return ( 43 | 49 |

Create New Product

50 | 51 |
52 |
53 | 56 | setNewProduct({ ...newProduct, name: e.target.value })} 62 | className='mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 63 | px-3 text-white focus:outline-none focus:ring-2 64 | focus:ring-emerald-500 focus:border-emerald-500' 65 | required 66 | /> 67 |
68 | 69 |
70 | 73 |