capstone 3 files

main
commit 69d3cd05d4

@ -0,0 +1,5 @@
PORT = 4006
MONGO_URI = 'mongodb+srv://FullStackCapstone3ByBr3y:FullStackCapstone3ByBr3y@ecomshop.rvywur0.mongodb.net/capstone3?retryWrites=true&w=majority&appName=AtlasApp'
JWT_SECRET = Capstone3FSByAubreyLizardo
NODE_ENV = development
PAYPAL_CLIENT_ID =AUf7NXbMHXLJQt_NDuakwDQSJlc8z_89G1soQOVhsge3W5qunLZhmBFXK8tKc89DfyJpSqPuZA3hd1Wn

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -0,0 +1,17 @@
# Zuitt Bootstrap Capstone 3 E-Commerce Web App
<!-- - setup
- install tailwind css and npm init -y
- npm i nodemon multer mongoose jsonwebtoken express-formidable express-async-handler express dotenv cors cookie-parser concurrently bcryptjs
- inside frontend folder
- npm i slick-carousel react-slick react-toastify react-router react-router-dom react-redux react-icons apexcharts react-apexcharts moment flowbite axios @reduxjs/toolkit @paypal/react-paypal-js -->
- setup from different computer
- npm install > cd frontend > npm install
- to run frontend: npm run frontend
- to run backend: npm run backend
- to run both: npm run dev
..

@ -0,0 +1,13 @@
import mongoose from "mongoose";
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI)
console.log(`Sucessfully connected to mongoDB`)
} catch(error){
console.log(`ERROR: ${error.message}`)
process.exit(1)
}
}
export default connectDB

@ -0,0 +1,91 @@
import Category from "../models/categoryModel.js";
import asyncHandler from "../middleware/asyncHandler.js";
const createCategory = asyncHandler(async (req, res) => {
try {
const { name } = req.body;
if (!name) {
return res.json({ error: "Name is totally required" });
}
const existingCategory = await Category.findOne({ name });
if (existingCategory) {
return res.json({ error: "Already exists" });
}
const category = await new Category({ name }).save()
res.json(category)
} catch (error) {
console.log(error);
return res.status(400).json(error);
}
});
const updateCategory = asyncHandler(async (req, res) => {
try {
const { name } = req.body;
const { categoryId } = req.params;
const category = await Category.findOne({ _id: categoryId });
if (!category) {
return res.status(404).json({ error: "Category not found" });
}
category.name = name;
const updatedCategory = await category.save();
res.json(updatedCategory);
} catch (error) {
console.error(error);
res.status(500).json({ error: "Internal server error" });
}
})
const removeCategory = asyncHandler(async(req, res) => {
try{
const removed = await Category.findByIdAndDelete(req.params.categoryId)
res.json(removed)
} catch(error){
console.log(error)
res.status(500).json({error: "Internal server error"})
}
})
const listCategory = asyncHandler(async(req, res) => {
try{
const allCategory = await Category.find({})
res.json(allCategory)
} catch(error){
console.log(error)
res.status(400).json(error.message)
}
})
const readCategory = asyncHandler(async(req, res) => {
try{
const category = await Category.findOne({_id: req.params.id})
res.json(category)
}catch(error){
console.log(error)
return res.status(400).json(error.message)
}
})
export {
createCategory,
updateCategory,
removeCategory,
listCategory,
readCategory
};

@ -0,0 +1,214 @@
import Order from "../models/orderModel.js";
import Product from "../models/productModel.js";
// Utility Function
function calcPrices(orderItems) {
const itemsPrice = orderItems.reduce(
(acc, item) => acc + item.price * item.qty,
0
);
const shippingPrice = itemsPrice > 100 ? 0 : 10;
const taxRate = 0.15;
const taxPrice = (itemsPrice * taxRate).toFixed(2);
const totalPrice = (
itemsPrice +
shippingPrice +
parseFloat(taxPrice)
).toFixed(2);
return {
itemsPrice: itemsPrice.toFixed(2),
shippingPrice: shippingPrice.toFixed(2),
taxPrice,
totalPrice,
};
}
const createOrder = async (req, res) => {
try {
const { orderItems, shippingAddress, paymentMethod } = req.body;
if (orderItems && orderItems.length === 0) {
res.status(400);
throw new Error("No order items");
}
const itemsFromDB = await Product.find({
_id: { $in: orderItems.map((x) => x._id) },
});
const dbOrderItems = orderItems.map((itemFromClient) => {
const matchingItemFromDB = itemsFromDB.find(
(itemFromDB) => itemFromDB._id.toString() === itemFromClient._id
);
if (!matchingItemFromDB) {
res.status(404);
throw new Error(`Product not found: ${itemFromClient._id}`);
}
return {
...itemFromClient,
product: itemFromClient._id,
price: matchingItemFromDB.price,
_id: undefined,
};
});
const { itemsPrice, taxPrice, shippingPrice, totalPrice } =
calcPrices(dbOrderItems);
const order = new Order({
orderItems: dbOrderItems,
user: req.user._id,
shippingAddress,
paymentMethod,
itemsPrice,
taxPrice,
shippingPrice,
totalPrice,
});
const createdOrder = await order.save();
res.status(201).json(createdOrder);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
const getAllOrders = async (req, res) => {
try {
const orders = await Order.find({}).populate("user", "id username");
res.json(orders);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
const getUserOrders = async (req, res) => {
try {
const orders = await Order.find({ user: req.user._id });
res.json(orders);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
const countTotalOrders = async (req, res) => {
try {
const totalOrders = await Order.countDocuments();
res.json({ totalOrders });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
const calculateTotalSales = async (req, res) => {
try {
const orders = await Order.find();
const totalSales = orders.reduce((sum, order) => sum + order.totalPrice, 0);
res.json({ totalSales });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
const calcualteTotalSalesByDate = async (req, res) => {
try {
const salesByDate = await Order.aggregate([
{
$match: {
isPaid: true,
},
},
{
$group: {
_id: {
$dateToString: { format: "%Y-%m-%d", date: "$paidAt" },
},
totalSales: { $sum: "$totalPrice" },
},
},
]);
res.json(salesByDate);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
const findOrderById = async (req, res) => {
try {
const order = await Order.findById(req.params.id).populate(
"user",
"username email"
);
if (order) {
res.json(order);
} else {
res.status(404);
throw new Error("Order not found");
}
} catch (error) {
res.status(500).json({ error: error.message });
}
};
const markOrderAsPaid = async (req, res) => {
try {
const order = await Order.findById(req.params.id);
if (order) {
order.isPaid = true;
order.paidAt = Date.now();
order.paymentResult = {
id: req.body.id,
status: req.body.status,
update_time: req.body.update_time,
email_address: req.body.payer.email_address,
};
const updateOrder = await order.save();
res.status(200).json(updateOrder);
} else {
res.status(404);
throw new Error("Order not found");
}
} catch (error) {
res.status(500).json({ error: error.message });
}
};
const markOrderAsDelivered = async (req, res) => {
try {
const order = await Order.findById(req.params.id);
if (order) {
order.isDelivered = true;
order.deliveredAt = Date.now();
const updatedOrder = await order.save();
res.json(updatedOrder);
} else {
res.status(404);
throw new Error("Order not found");
}
} catch (error) {
res.status(500).json({ error: error.message });
}
};
export {
createOrder,
getAllOrders,
getUserOrders,
countTotalOrders,
calculateTotalSales,
calcualteTotalSalesByDate,
findOrderById,
markOrderAsPaid,
markOrderAsDelivered,
};

@ -0,0 +1,220 @@
import express from "express";
import asyncHandler from "../middleware/asyncHandler.js";
import Product from "../models/productModel.js";
const addProduct = asyncHandler(async (req, res) => {
try {
const { name, description, price, category, quantity, brand } = req.fields;
// Validation
switch (true) {
case !name:
return res.json({ error: "Name is required" });
case !brand:
return res.json({ error: "Brand is required" });
case !description:
return res.json({ error: "Description is required" });
case !price:
return res.json({ error: "Price is required" });
case !category:
return res.json({ error: "Category is required" });
case !quantity:
return res.json({ error: "Quantity is required" });
}
const product = new Product({ ...req.fields });
await product.save();
res.json(product);
} catch (error) {
console.log(error);
res.status(400).json(error.message);
}
});
const updateProductDetails = asyncHandler(async (req, res) => {
try {
const { name, description, price, category, quantity, brand } = req.fields;
// Validation
switch (true) {
case !name:
return res.json({ error: "Name is required" });
case !brand:
return res.json({ error: "Brand is required" });
case !description:
return res.json({ error: "Description is required" });
case !price:
return res.json({ error: "Price is required" });
case !category:
return res.json({ error: "Category is required" });
case !quantity:
return res.json({ error: "Quantity is required" });
}
const product = await Product.findByIdAndUpdate(
req.params.id,
{ ...req.fields },
{ new: true }
);
await product.save();
res.json(product);
} catch (error) {
console.log(error);
res.status(400).json(error.message);
}
});
const removeProduct = asyncHandler(async (req, res) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
res.json(product);
} catch (error) {
console.log(error);
res.status(500).json({ error: "Server error" });
}
});
const getAllProducts = asyncHandler(async (req, res) => {
try {
const pageSize = 6;
const keyword = req.query.keyword
? { name: { $regex: req.query.keyword, $options: "i" } }
: {};
const count = await Product.countDocuments({ ...keyword });
const products = await Product.find({ ...keyword }).limit(pageSize);
res.json({
products,
page: 1,
pages: Math.ceil(count / pageSize),
hasMore: false,
});
} catch (error) {
console.log(error);
res.status(500).json({ error: "Server error" });
}
});
const getProductById = asyncHandler(async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (product) {
return res.json(product);
} else {
res.status(404);
throw new Error("Product not found");
}
} catch (error) {
console.log(error);
res.status(404).json({ error: "Product not found" });
}
});
const fetchAllProducts = asyncHandler(async (req, res) => {
try {
const products = await Product.find({})
.populate("category")
.limit(12)
.sort({ createAt: -1 });
res.json(products);
} catch (error) {
console.log(error);
res.status(500).json({ error: "Server Error" });
}
});
const addProductReview = asyncHandler(async (req, res) => {
try {
const { rating, comment } = req.body;
const product = await Product.findById(req.params.id);
if (product) {
const alreadyReviewed = product.reviews.find(
(review) => review.user.toString() === req.user._id.toString()
);
if (alreadyReviewed) {
res.status(400);
throw new Error("Product already reviewed");
}
const review = {
name: req.user.username,
rating: Number(rating),
comment,
user: req.user._id,
};
product.reviews.push(review);
product.numReviews = product.reviews.length;
product.rating =
product.reviews.reduce((acc, item) => item.rating + acc, 0) /
product.reviews.length;
await product.save();
res.status(201).json({ message: "Review added" });
} else {
res.status(404);
throw new Error("Product not found");
}
} catch (error) {
console.log(error);
res.status(400).json(error.message);
}
});
const fetchTopProducts = asyncHandler(async (req, res) => {
try {
const products = await Product.find({}).sort({ rating: -1 }).limit(4);
res.json(products);
} catch (error) {
console.log(error);
res.status(400).json(error.message);
}
});
const fetchNewProducts = asyncHandler(async (req, res) => {
try {
const products = await Product.find().sort({ _id: -1 }).limit(5);
res.json(products);
} catch (error) {
console.log(error);
res.status(400).json(error.message);
}
});
const filterProducts = asyncHandler(async (req, res) => {
try {
const { checked, radio } = req.body;
let args = {};
if (checked.length > 0) args.category = checked;
if (radio.length) args.price = { $gte: radio[0], $lte: radio[1] };
const products = await Product.find(args);
res.json(products);
} catch (error) {
console.error(error);
res.status(500).json({ error: "Server Error" });
}
});
export {
addProduct,
updateProductDetails,
removeProduct,
getAllProducts,
getProductById,
fetchAllProducts,
addProductReview,
fetchTopProducts,
fetchNewProducts,
filterProducts,
};

@ -0,0 +1,191 @@
import User from '../models/userModel.js'
import asyncHandler from '../middleware/asyncHandler.js'
import bcrypt from 'bcryptjs'
import createToken from '../utils/createToken.js'
const createUser = asyncHandler(async (req, res) => {
const { username, email, password, isAdmin } = req.body
if (!username || !email || !password) {
throw new Error('Please fill all the inputs')
}
const userExists = await User.findOne({ email })
if (userExists) {
res.status(400)
throw new Error("User already exist")
}
const salt = await bcrypt.genSalt(10)
const hashedPassword = await bcrypt.hash(password, salt)
const newUser = new User({
username,
email,
password: hashedPassword,
isAdmin: isAdmin
})
try {
await newUser.save()
// generateToken
createToken(res, newUser._id)
res.status(201).json({
_id: newUser._id,
username: newUser.username,
email: newUser.email,
isAdmin: newUser.isAdmin
})
} catch (error) {
res.status(400)
throw new Error("Invalid user data")
}
})
const loginUser = asyncHandler(async (req, res) => {
const { email, password } = req.body
const existingUser = await User.findOne({ email })
if (existingUser) {
const isPasswordValid = await bcrypt.compare(password, existingUser.password)
if (isPasswordValid) {
createToken(res, existingUser._id)
res.status(201).json({
_id: existingUser._id,
username: existingUser.username,
email: existingUser.email,
isAdmin: existingUser.isAdmin
})
} else {
res.status(400)
throw new Error("Wrong Password")
}
}
})
const logoutCurrentUser = asyncHandler(async (req, res) => {
res.cookie('jwt', '', {
httpOnly: true,
expires: new Date(0)
})
res.status(200).json({ message: "Logged out sucessfully -cookie removed-" })
})
const getAllUsers = asyncHandler(async (req, res) => {
const users = await User.find({})
res.json(users)
})
const getCurrentUserProfile = asyncHandler(async (req, res) => {
const user = await User.findById(req.user._id)
if (user) {
res.json({
_id: user._id,
username: user.username,
email: user.email
})
} else {
res.status(404)
throw new Error("User not found")
}
})
const updateCurrentUserProfile = asyncHandler(async (req, res) => {
const user = await User.findById(req.user._id)
if (user) {
user.username = req.body.username || user.username
user.email = req.body.email || user.email
if (req.body.password) {
const salt = await bcrypt.genSalt(10)
const hashedPassword = await bcrypt.hash(req.body.password, salt)
user.password = hashedPassword
}
const updatedUser = await user.save()
res.json({
_id: updatedUser._id,
username: updatedUser.username,
email: updatedUser.email,
isAdmin: updatedUser.isAdmin
})
} else {
res.status(404)
throw new Error("User not found")
}
})
const deletUserById = asyncHandler(async(req, res) => {
const user = await User.findById(req.params.id)
if(user){
if(user.isAdmin){
res.status(400)
throw new Error('Cannot delete admin user')
}
await User.deleteOne({_id: user._id})
res.json({message: "User removed"})
} else {
res.status(404)
throw new Error("User not found")
}
})
const getUserById = asyncHandler(async(req, res) => {
const user = await User.findById(req.params.id).select('-password')
if(user){
res.json(user)
} else {
res.status(404)
throw new Error("User not found")
}
})
const updateUserById = asyncHandler(async(req, res) => {{
const user = await User.findById(req.params.id)
if(user){
user.username = req.body.username || user.username
user.email = req.body.email || user.email
user.isAdmin = Boolean(req.body.isAdmin)
const updatedUser = await user.save()
res.json({
_id: updatedUser._id,
username: updatedUser.username,
email:updatedUser.email,
isAdmin: updatedUser.isAdmin
})
} else {
res.status(404)
throw new Error("User not found")
}
}})
export {
createUser,
loginUser,
logoutCurrentUser,
getAllUsers,
getCurrentUserProfile,
updateCurrentUserProfile,
deletUserById,
getUserById,
updateUserById
}

@ -0,0 +1,39 @@
// packages
import path from 'path'
import express from 'express'
import dotenv from 'dotenv'
import cookieParser from 'cookie-parser'
// utils
import connectDB from './config/db.js'
import userRoute from './routes/userRoute.js'
import categoryRoute from './routes/categoryRoute.js'
import productRoute from './routes/productRoute.js'
import uploadRoute from './routes/uploadRoute.js'
import orderRoutes from "./routes/orderRoute.js";
dotenv.config()
const port = process.env.PORT || 4000
connectDB()
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(cookieParser())
app.use('/b6/users', userRoute)
app.use('/b6/category', categoryRoute)
app.use('/b6/products', productRoute)
app.use('/b6/upload', uploadRoute)
app.use("/b6/orders", orderRoutes);
app.get('/b6/config/paypal', (req, res) => {
res.send({clientId: process.env.PAYPAL_CLIENT_ID})
})
const __dirname = path.resolve()
app.use('/uploads', express.static(path.join(__dirname, '/uploads')))
app.listen(port, () => console.log(`Server running on port: ${port}`))

@ -0,0 +1,7 @@
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(error => {
res.status(500).json({message: error.message})
})
}
export default asyncHandler

@ -0,0 +1,38 @@
import jwt from 'jsonwebtoken'
import User from '../models/userModel.js'
import asyncHandler from './asyncHandler.js'
const authenticate = asyncHandler(async(req, res, next) => {
let token
// Read JWT from the 'jwt' cookie
token = req.cookies.jwt
if(token) {
try{
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = await User.findById(decoded.userId).select('-password')
next()
} catch(error){
res.status(401)
throw new Error("Not authorized, token failed.")
}
} else {
res.status(401)
throw new Error("Not authorized, no token.")
}
})
// Check if user is admin
const authorizeAdmin = (req, res, next) => {
if(req.user.isAdmin){
next()
} else {
res.status(401).send("Not authorized as an admin.")
}
}
export { authenticate, authorizeAdmin }

@ -0,0 +1,12 @@
import { isValidObjectId } from "mongoose";
function checkId( req, res, next ) {
if(!isValidObjectId(req.params.id)){
res.status(404)
throw new Error(`Invalid Object of: ${req.params.id}`)
}
next()
}
export default checkId

@ -0,0 +1,14 @@
import mongoose from "mongoose";
const categorySchema = new mongoose.Schema({
name: {
type: String,
trim: true,
require: true,
maxLength: 32,
unique: true,
}
})
export default mongoose.model('Category', categorySchema)

@ -0,0 +1,89 @@
import mongoose from "mongoose";
const orderSchema = mongoose.Schema(
{
user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: "User" },
orderItems: [
{
name: { type: String, required: true },
qty: { type: Number, required: true },
image: { type: String, required: true },
price: { type: Number, required: true },
product: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "Product",
},
},
],
shippingAddress: {
address: { type: String, required: true },
city: { type: String, required: true },
postalCode: { type: String, required: true },
country: { type: String, required: true },
},
paymentMethod: {
type: String,
required: true,
},
paymentResult: {
id: { type: String },
status: { type: String },
update_time: { type: String },
email_address: { type: String },
},
itemsPrice: {
type: Number,
required: true,
default: 0.0,
},
taxPrice: {
type: Number,
required: true,
default: 0.0,
},
shippingPrice: {
type: Number,
required: true,
default: 0.0,
},
totalPrice: {
type: Number,
required: true,
default: 0.0,
},
isPaid: {
type: Boolean,
required: true,
default: false,
},
paidAt: {
type: Date,
},
isDelivered: {
type: Boolean,
required: true,
default: false,
},
deliveredAt: {
type: Date,
},
},
{
timestamps: true,
}
);
const Order = mongoose.model("Order", orderSchema);
export default Order;

@ -0,0 +1,77 @@
import mongoose from "mongoose";
const { ObjectId } = mongoose.Schema;
const reviewSchema = mongoose.Schema(
{
name: {
type: String,
required: true,
},
rating: {
type: Number,
required: true,
},
comment: {
type: String,
required: true,
},
user: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "User",
},
},
{ timestamps: true }
);
const productSchema = mongoose.Schema(
{
name: {
type: String,
required: true,
},
image: {
type: String,
required: true,
},
brand: {
type: String,
required: true,
},
quantity: {
type: Number,
required: true,
},
category: {
type: ObjectId,
ref: "Category",
required: true,
},
description: { type: String, required: true },
reviews: [reviewSchema],
rating: {
type: Number,
required: true,
default: 0,
},
numReviews: {
type: Number,
required: true,
default: 0,
},
price: {
type: Number,
required: true,
default: 0,
},
countInStock: {
type: Number,
required: true,
default: 0,
},
},
{ timestamps: true }
);
const Product = mongoose.model("Product", productSchema);
export default Product;

@ -0,0 +1,26 @@
import mongoose from "mongoose";
const userSchema = mongoose.Schema({
username: {
type:String,
required: true,
},
email: {
type:String,
required:true,
unique: true
},
password: {
type:String,
required: true,
},
isAdmin: {
type:Boolean,
required: true,
default: false
}
}, { timestamps: true })
const User = mongoose.model('USer', userSchema)
export default User

@ -0,0 +1,25 @@
import express from "express";
import {
createCategory,
updateCategory,
removeCategory,
listCategory,
readCategory
} from "../controllers/categoryController.js";
import { authenticate, authorizeAdmin } from "../middleware/authMiddleWare.js";
const router = express.Router();
router.route("/").post(authenticate, authorizeAdmin, createCategory);
router
.route("/:categoryId")
.put(authenticate, authorizeAdmin, updateCategory)
.delete(authenticate, authorizeAdmin, removeCategory)
router
.route("/categories")
.get(listCategory)
router
.route("/:id")
.get(readCategory)
export default router;

@ -0,0 +1,33 @@
import express from "express";
const router = express.Router();
import {
createOrder,
getAllOrders,
getUserOrders,
countTotalOrders,
calculateTotalSales,
calcualteTotalSalesByDate,
findOrderById,
markOrderAsPaid,
markOrderAsDelivered,
} from "../controllers/orderController.js";
import { authenticate, authorizeAdmin } from "../middleware/authMiddleWare.js";
router
.route("/")
.post(authenticate, createOrder)
.get(authenticate, authorizeAdmin, getAllOrders);
router.route("/mine").get(authenticate, getUserOrders);
router.route("/total-orders").get(countTotalOrders);
router.route("/total-sales").get(calculateTotalSales);
router.route("/total-sales-by-date").get(calcualteTotalSalesByDate);
router.route("/:id").get(authenticate, findOrderById);
router.route("/:id/pay").put(authenticate, markOrderAsPaid);
router
.route("/:id/deliver")
.put(authenticate, authorizeAdmin, markOrderAsDelivered);
export default router;

@ -0,0 +1,39 @@
import express from "express";
import formidable from "express-formidable";
const router = express.Router();
import {
addProduct,
updateProductDetails,
removeProduct,
getAllProducts,
getProductById,
fetchAllProducts,
addProductReview,
fetchTopProducts,
fetchNewProducts,
filterProducts,
} from "../controllers/productController.js";
import { authenticate, authorizeAdmin } from "../middleware/authMiddleWare.js";
import checkId from "../middleware/checkId.js";
router.route("/fetchAllProducts").get(fetchAllProducts);
router.route("/:id/reviews").post(authenticate, checkId, addProductReview);
router.route("/top").get(fetchTopProducts);
router.route("/new").get(fetchNewProducts);
router
.route("/")
.post(authenticate, authorizeAdmin, formidable(), addProduct)
.get(getAllProducts);
router
.route("/:id")
.get(getProductById)
.put(authenticate, authorizeAdmin, formidable(), updateProductDetails)
.delete(authenticate, authorizeAdmin, removeProduct);
router.route("/filtered-products").post(filterProducts);
export default router;

@ -0,0 +1,50 @@
import path from "path";
import express from "express";
import multer from "multer";
const router = express.Router();
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
const extname = path.extname(file.originalname);
cb(null, `${file.fieldname}-${Date.now()}${extname}`);
},
});
const fileFilter = (req, file, cb) => {
const filetypes = /jpe?g|png|webp/;
const mimetypes = /image\/jpe?g|image\/png|image\/webp/;
const extname = path.extname(file.originalname).toLowerCase();
const mimetype = file.mimetype;
if (filetypes.test(extname) && mimetypes.test(mimetype)) {
cb(null, true);
} else {
cb(new Error("Images only"), false);
}
};
const upload = multer({ storage, fileFilter });
const uploadSingleImage = upload.single("image");
router.post("/", (req, res) => {
uploadSingleImage(req, res, (err) => {
if (err) {
res.status(400).send({ message: err.message });
} else if (req.file) {
res.status(200).send({
message: "Image uploaded successfully",
image: `/${req.file.path}`,
});
} else {
res.status(400).send({ message: "No image file provided" });
}
});
});
export default router;

@ -0,0 +1,35 @@
import express from "express";
import {
createUser,
loginUser,
logoutCurrentUser,
getAllUsers,
getCurrentUserProfile,
updateCurrentUserProfile,
deletUserById,
getUserById,
updateUserById,
} from "../controllers/userController.js";
import { authenticate, authorizeAdmin } from "../middleware/authMiddleWare.js";
const router = express.Router();
router.post("/register", createUser);
router.post("/login", loginUser);
router.post("/logout", logoutCurrentUser);
router
.route("/profile")
.get(authenticate, getCurrentUserProfile)
.put(authenticate, updateCurrentUserProfile);
router.get("/getAllUsers", authenticate, authorizeAdmin, getAllUsers);
router
.route("/:id")
.delete(authenticate, authorizeAdmin, deletUserById)
.get(authenticate, authorizeAdmin, getUserById)
.put(authenticate, authorizeAdmin, updateUserById);
export default router;

@ -0,0 +1,18 @@
import jwt from 'jsonwebtoken'
const generateToken = (res, userId) => {
const token = jwt.sign({userId}, process.env.JWT_SECRET, { expiresIn: "30d"})
// Set JWT as HTTP-Only Cookie
res.cookie('jwt', token, {
httpOnly: true,
secure: process.env.NODE_ENV !== 'development',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 100
})
return token
}
export default generateToken

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@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
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Capstone 3FS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,43 @@
{
"name": "capstone3fs",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@paypal/react-paypal-js": "^8.1.3",
"@reduxjs/toolkit": "^1.9.7",
"apexcharts": "^3.44.0",
"axios": "^1.6.2",
"flowbite": "^2.1.1",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-apexcharts": "^1.4.1",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
"react-redux": "^8.1.3",
"react-router": "^6.19.0",
"react-router-dom": "^6.19.0",
"react-slick": "^0.29.0",
"react-toastify": "^9.1.3",
"slick-carousel": "^1.8.1"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"vite": "^5.0.0"
}
}

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

@ -0,0 +1,19 @@
import { Outlet } from 'react-router-dom'
import Navigation from './pages/Auth/Navigation'
import { ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
function App() {
return (
<>
<ToastContainer />
<Navigation />
<main className='py-3'>
<Outlet />
</main>
</>
)
}
export default App

@ -0,0 +1,49 @@
import { Link, useParams } from "react-router-dom";
import { useGetProductsQuery } from "./redux/api/productApiSlice";
import Loader from "./components/Loader";
import Header from "./components/Header";
import Message from "./components/Message";
import Product from "./pages/Products/Product";
const Home = () => {
const { keyword } = useParams();
const { data, isLoading, isError } = useGetProductsQuery({ keyword });
return (
<>
{!keyword ? <Header /> : null}
{isLoading ? (
<Loader />
) : isError ? (
<Message variant="danger">
{isError?.data.message || isError.error}
</Message>
) : (
<>
<div className="flex justify-between items-center">
<h1 className="ml-[20rem] mt-[10rem] text-[3rem]">
Special Product
</h1>
<Link
to="/shop"
className="bg-pink-600 text-white font-bold rounded-full py-2 px-10 mr-[18rem] mt-[10rem]"
>
Shop
</Link>
</div>
<div className="flex justify-center flex-wrap mt-[2rem]">
{data.products.map((product) => (
<div key={product._id}>
<Product product={product} />
</div>
))}
</div>
</>
)}
</>
);
};
export default Home;

@ -0,0 +1,28 @@
export const addDecimals = (num) => {
return (Math.round(num * 100) / 100).toFixed(2);
};
export const updateCart = (state) => {
// Calculate the items price
state.itemsPrice = addDecimals(
state.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)
);
// Calculate the shipping price
state.shippingPrice = addDecimals(state.itemsPrice > 100 ? 0 : 10);
// Calculate the tax price
state.taxPrice = addDecimals(Number((0.15 * state.itemsPrice).toFixed(2)));
// Calculate the total price
state.totalPrice = (
Number(state.itemsPrice) +
Number(state.shippingPrice) +
Number(state.taxPrice)
).toFixed(2);
// Save the cart to localStorage
localStorage.setItem("cart", JSON.stringify(state));
return state;
};

@ -0,0 +1,23 @@
// Add a product to a localStorage
export const addFavoritesToLocalStorage = (product) => {
const favorites = getFavoritesFromLocalStorage();
if (!favorites.some((p) => p._id === product._id)) {
favorites.push(product);
localStorage.setItem("favorites", JSON.stringify(favorites));
}
};
// Remove
export const removeFavoritesFromLocalStorage = (productId) => {
const favorites = getFavoritesFromLocalStorage();
const updateFavorites = favorites.filter(
(product) => product._id !== productId
);
localStorage.setItem("favorites", JSON.stringify(updateFavorites));
};
// Retrieve
export const getFavoritesFromLocalStorage = () => {
const favoritesJSON = localStorage.getItem("favorites");
return favoritesJSON ? JSON.parse(favoritesJSON) : [];
};

@ -0,0 +1,35 @@
const CategoryForm = ({
value,
setValue,
handleSubmit,
buttonText = "Submit",
handleDelete,
}) => {
return (
<div className="p-3">
<form onSubmit={handleSubmit} className="space-y-3">
<input
type="text"
className="py-3 px-4 border rounded-lg w-full"
placeholder="Write category name"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<div className="flex justify-between">
<button className="bg-pink-500 text-white py-2 px-4 rounded-lg hover:bg-pink-600 focus:outline-none focus:ring-2 focus:ring-pink-500 focus:ring-opacity-50">
{buttonText}
</button>
{handleDelete && (
<button onClick={handleDelete} className="bg-red-500 text-white py-2 px-4 rounded-lg hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
Delete
</button>
)}
</div>
</form>
</div>
);
};
export default CategoryForm;

@ -0,0 +1,35 @@
import { useGetTopProductQuery } from "../redux/api/productApiSlice";
import Loader from "./Loader";
import SmallProduct from '../pages/Products/SmallProduct'
import ProductCarousel from "../pages/Products/ProductCarousel";
const Header = () => {
const { data, isLoading, error } = useGetTopProductQuery();
if (isLoading) {
return <Loader />;
}
if (error) {
return <h1>Error</h1>;
}
return (
<>
<div className="flex justify-around">
<div className="xl:block lg:hidden md:hidden sm:hidden">
<div className="grid grid-cols-2">
{data.map((product) => (
<div key={product._id}>
<SmallProduct product={product} />
</div>
))}
</div>
</div>
<ProductCarousel />
</div>
</>
);
};
export default Header;

@ -0,0 +1,9 @@
const Loader = () => {
return (
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-pink-500 border-opacity-50">
</div>
);
};
export default Loader;

@ -0,0 +1,15 @@
const Message = ({ variant, children }) => {
const getVariantClass = () => {
switch (variant) {
case "success":
return "bg-green-100 text-green-800";
case "error":
return "bg-red-100 text-red-800";
default:
return "bg-blue-100 text-blue-800";
}
};
return <div className={`p-4 rounded ${getVariantClass()}`}>{children}</div>;
};
export default Message;

@ -0,0 +1,22 @@
const Modal = ({ isOpen, onClose, children }) => {
return (
<>
{isOpen && (
<div className="fixed inset-0 flex items-center justify-center z-50">
<div className="fixed inset-0 bg-black opacity-50"></div>
<div className="absolute top-[40%] right-[50%] bg-white p-4 rounded-lg z-10 text-right">
<button
onClick={onClose}
className="text-black font-semibold hover:text-gray-700 focus:outline-none mr-2"
>
-X-
</button>
{children}
</div>
</div>
)}
</>
);
};
export default Modal;

@ -0,0 +1,11 @@
import {Navigate, Outlet} from 'react-router-dom'
import {useSelector} from 'react-redux'
const PrivateRoute = () => {
const { userInfo } = useSelector(state => state.auth)
return (
userInfo ? <Outlet /> : <Navigate to='/login' replace />
)
}
export default PrivateRoute

@ -0,0 +1,39 @@
const ProgressSteps = ({ step1, step2, step3 }) => {
return (
<div className="flex justify-center items-center space-x-4">
<div className={`${step1 ? "text-green-500" : "text-gray-300"}`}>
<span className="ml-2">Login</span>
<div className="mt-2 text-lg text-center"></div>
</div>
{step2 && (
<>
{step1 && <div className="h-0.5 w-[10rem] bg-green-500"></div>}
<div className={`${step1 ? "text-green-500" : "text-gray-300"}`}>
<span>Shipping</span>
<div className="mt-2 text-lg text-center"></div>
</div>
</>
)}
<>
{step1 && step2 && step3 ? (
<div className="h-0.5 w-[10rem] bg-green-500"></div>
) : (
""
)}
<div className={`${step3 ? "text-green-500" : "text-gray-300"}`}>
<span className={`${!step3 ? "ml-[10rem]" : ""}`}>Summary</span>
{step1 && step2 && step3 ? (
<div className="mt-2 text-lg text-center"></div>
) : (
""
)}
</div>
</>
</div>
);
};
export default ProgressSteps;

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1,75 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { Route, RouterProvider, createRoutesFromElements } from "react-router";
import { createBrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./redux/store.js";
// Private Route
import PrivateRoute from "./components/PrivateRoute.jsx";
// Auth
import Login from "./pages/Auth/Login.jsx";
import Register from "./pages/Auth/Register.jsx";
import Profile from "./pages/User/Profile.jsx";
import AdminRoute from "./pages/Admin/AdminRoute.jsx";
import UserList from "./pages/Admin/UserList.jsx";
import CategoryList from "./pages/Admin/CategoryList.jsx";
import ProductList from "./pages/Admin/ProductList";
import AllProducts from "./pages/Admin/AllProducts";
import ProductUpdate from "./pages/Admin/ProductUpdate";
import Home from "./Home.jsx";
import Favorites from "./pages/Products/Favorites.jsx";
import ProductDetails from "./pages/Products/ProductDetails.jsx";
import Cart from "./pages/Cart.jsx";
import Shop from "./pages/Shop.jsx";
import Shipping from "./pages/Orders/Shipping.jsx";
import PlaceOrder from "./pages/Orders/PlaceOrder.jsx";
import Order from "./pages/Orders/Order.jsx";
import UserOrder from "./pages/User/UserOrder.jsx"
import { PayPalScriptProvider } from '@paypal/react-paypal-js'
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<App />}>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route index={true} path="/" element={<Home />} />
<Route path="favorite" element={<Favorites />} />
<Route path="/product/:id" element={<ProductDetails />} />
<Route path="/cart" element={<Cart />} />
<Route path="/shop" element={<Shop />} />
<Route path="/user-orders" element={<UserOrder />} />
<Route path="" element={<PrivateRoute />}>
<Route path="/profile" element={<Profile />} />
<Route path="/shipping" element={<Shipping />} />
<Route path="/placeorder" element={<PlaceOrder />} />
<Route path="/order/:id" element={<Order />} />
</Route>
{/* Admin */}
<Route path="/admin" element={<AdminRoute />}>
<Route path="userlist" element={<UserList />} />
<Route path="categorylist" element={<CategoryList />} />
<Route path="productlist" element={<ProductList />} />
<Route path="allproductslist" element={<AllProducts />} />
<Route path="productlist/:pageNumber" element={<ProductList />} />
<Route path="product/update/:_id" element={<ProductUpdate />} />
</Route>
</Route>
)
);
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<PayPalScriptProvider>
<RouterProvider router={router} />
</PayPalScriptProvider>
</Provider>
);

@ -0,0 +1,107 @@
import { useState } from "react";
import { NavLink } from "react-router-dom";
import { FaTimes } from "react-icons/fa";
const AdminMenu = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
return (
<>
<button
className={`${
isMenuOpen ? "top-2 right-2" : "top-5 right-7"
} bg-[#151515] p-2 fixed rounded-lg`}
onClick={toggleMenu}
>
{isMenuOpen ? (
<FaTimes color="white" />
) : (
<>
<div className="w-6 h-0.5 bg-gray-200 my-1"></div>
<div className="w-6 h-0.5 bg-gray-200 my-1"></div>
<div className="w-6 h-0.5 bg-gray-200 my-1"></div>
</>
)}
</button>
{isMenuOpen && (
<section className="bg-[#151515] p-4 fixed right-2 top-8">
<ul className="list-none mt-2">
<li>
<NavLink
className="py-2 px-3 block mb-5 hover:bg-[#2E2D2D] rounded-sm"
to="/admin/dashboard"
style={({ isActive }) => ({
color: isActive ? "greenyellow" : "white",
})}
>
Admin Dashboard
</NavLink>
</li>
<li>
<NavLink
className="py-2 px-3 block mb-5 hover:bg-[#2E2D2D] rounded-sm"
to="/admin/categorylist"
style={({ isActive }) => ({
color: isActive ? "greenyellow" : "white",
})}
>
Create Category
</NavLink>
</li>
<li>
<NavLink
className="py-2 px-3 block mb-5 hover:bg-[#2E2D2D] rounded-sm"
to="/admin/productlist"
style={({ isActive }) => ({
color: isActive ? "greenyellow" : "white",
})}
>
Create Product
</NavLink>
</li>
<li>
<NavLink
className="py-2 px-3 block mb-5 hover:bg-[#2E2D2D] rounded-sm"
to="/admin/allproductslist"
style={({ isActive }) => ({
color: isActive ? "greenyellow" : "white",
})}
>
All Products
</NavLink>
</li>
<li>
<NavLink
className="py-2 px-3 block mb-5 hover:bg-[#2E2D2D] rounded-sm"
to="/admin/userlist"
style={({ isActive }) => ({
color: isActive ? "greenyellow" : "white",
})}
>
Manage Users
</NavLink>
</li>
<li>
<NavLink
className="py-2 px-3 block mb-5 hover:bg-[#2E2D2D] rounded-sm"
to="/admin/orderlist"
style={({ isActive }) => ({
color: isActive ? "greenyellow" : "white",
})}
>
Manage Orders
</NavLink>
</li>
</ul>
</section>
)}
</>
);
};
export default AdminMenu;

@ -0,0 +1,15 @@
import { Navigate, Outlet } from "react-router";
import { useSelector } from "react-redux";
const AdminRoute = () => {
const { userInfo } = useSelector((state) => state.auth);
return userInfo && userInfo.isAdmin ? (
<Outlet />
) : (
<Navigate to="/login" replace />
);
};
export default AdminRoute;

@ -0,0 +1,92 @@
import { Link } from "react-router-dom";
import moment from "moment";
import { useAllProductsQuery } from "../../redux/api/productApiSlice";
import AdminMenu from "./AdminMenu";
const AllProducts = () => {
const { data: products, isLoading, isError } = useAllProductsQuery();
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error loading products</div>;
}
return (
<>
<div className="container mx-[9rem]">
<div className="flex flex-col md:flex-row">
<div className="p-3">
<div className="ml-[2rem] text-xl font-bold h-12">
All Products ({products.length})
</div>
<div className="flex flex-wrap justify-around items-center">
{products.map((product) => (
<Link
key={product._id}
to={`/admin/product/update/${product._id}`}
className="block mb-4 overflow-hidden"
>
<div className="flex">
<img
src={product.image}
alt={product.name}
className="w-[10rem] object-cover"
/>
<div className="p-4 flex flex-col justify-around">
<div className="flex justify-between">
<h5 className="text-xl font-semibold mb-2">
{product?.name}
</h5>
<p className="text-gray-400 text-xs">
{moment(product.createdAt).format("MMMM Do YYYY")}
</p>
</div>
<p className="text-gray-400 xl:w-[30rem] lg:w-[30rem] md:w-[20rem] sm:w-[10rem] text-sm mb-4">
{product?.description?.substring(0, 160)}...
</p>
<div className="flex justify-between">
<Link
to={`/admin/product/update/${product._id}`}
className="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-pink-700 rounded-lg hover:bg-pink-800 focus:ring-4 focus:outline-none focus:ring-pink-300 dark:bg-pink-600 dark:hover:bg-pink-700 dark:focus:ring-pink-800"
>
Update Product
<svg
className="w-3.5 h-3.5 ml-2"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M1 5h12m0 0L9 1m4 4L9 9"
/>
</svg>
</Link>
<p>$ {product?.price}</p>
</div>
</div>
</div>
</Link>
))}
</div>
</div>
<div className="md:w-1/4 p-3 mt-2">
<AdminMenu />
</div>
</div>
</div>
</>
);
};
export default AllProducts;

@ -0,0 +1,137 @@
import { useState } from "react";
import {
useCreateCategoryMutation,
useUpdateCategoryMutation,
useDeleteCategoryMutation,
useFetchCategoriesQuery,
} from "../../redux/api/categoryApiSlice";
import { toast } from "react-toastify";
import CategoryForm from "../../components/CategoryForm";
import Modal from "../../components/Modal";
import AdminMenu from './AdminMenu'
const CategoryList = () => {
const { data: categories } = useFetchCategoriesQuery();
const [name, setName] = useState("");
const [selectedCategory, setSelectedCategory] = useState(null);
const [updatingName, setUpdatingName] = useState("");
const [modalVisible, setModalVisible] = useState(false);
const [createCategory] = useCreateCategoryMutation();
const [updateCategory] = useUpdateCategoryMutation();
const [deleteCategory] = useDeleteCategoryMutation();
const handleCreateCategory = async (e) => {
e.preventDefault();
if (!name) {
toast.error("Category is required");
return;
}
try {
const result = await createCategory({ name }).unwrap();
if (result.error) {
toast.error(result.error);
} else {
setName("");
toast.success(`${result.name} is created.`);
}
} catch (error) {
console.log(error);
toast.error("Creating category failed...");
}
};
const handleUpdateCategory = async (e) => {
e.preventDefault();
if (!updatingName) {
toast.error("Category name is required");
return;
}
try {
const result = await updateCategory({
categoryId: selectedCategory._id,
updatedCategory: {
name: updatingName,
},
}).unwrap();
if (result.error) {
toast.error(result.error);
} else {
toast.success(`${result.name} is updated`);
selectedCategory(null);
setUpdatingName("");
setModalVisible(false);
}
} catch (error) {
console.log(error);
}
};
const handleDeleteCategory = async () => {
try {
const result = await deleteCategory(selectedCategory._id).unwrap();
if (result.error) {
toast.error(result.error);
} else {
toast.success(`${result.name} is deleted.`);
setSelectedCategory(null);
setModalVisible(false);
}
} catch (error) {
console.log(error);
toast.error("Category delection failed. Try again.");
}
};
return (
<div className="flex flex-col md:flex-row justify-center ">
<AdminMenu />
<div className="md:w-3/4 p-3">
<h1 className="text-2xl font-semibold mb-4">Manage Categories</h1>
<CategoryForm
value={name}
setValue={setName}
handleSubmit={handleCreateCategory}
/>
<br />
<hr />
<div className="flex flex-wrap">
{categories?.map((category) => (
<div key={category._id}>
<button
onClick={() => {
{
setModalVisible(true);
setSelectedCategory(category);
setUpdatingName(category.name);
}
}}
className="bg-white border border-pink-500 text-pink-500 py-2 px-4 rounded-lg m-3 hover:bg-pink-500 hover:text-white focus:outline-none focus:ring-2 focus:ring-pink-500 focus:ring-opacity-50"
>
{category.name}
</button>
</div>
))}
</div>
<Modal isOpen={modalVisible} onClose={() => setModalVisible(false)}>
<CategoryForm
value={updatingName}
setValue={(value) => setUpdatingName(value)}
handleSubmit={handleUpdateCategory}
buttonText="Update"
handleDelete={handleDeleteCategory}
/>
</Modal>
</div>
</div>
);
};
export default CategoryList;

@ -0,0 +1,194 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
useCreateProductMutation,
useUploadProductImageMutation,
} from "../../redux/api/productApiSlice";
import { useFetchCategoriesQuery } from "../../redux/api/categoryApiSlice";
import { toast } from "react-toastify";
import AdminMenu from "./AdminMenu";
const ProductList = () => {
const [image, setImage] = useState("");
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [price, setPrice] = useState("");
const [category, setCategory] = useState("");
const [quantity, setQuantity] = useState("");
const [brand, setBrand] = useState("");
const [stock, setStock] = useState(0);
const [imageUrl, setImageUrl] = useState(null);
const navigate = useNavigate();
const [useUploadProductImage] = useUploadProductImageMutation();
const [createProduct] = useCreateProductMutation();
const { data: categories } = useFetchCategoriesQuery();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const productData = new FormData();
productData.append("image", image);
productData.append("name", name);
productData.append("description", description);
productData.append("price", price);
productData.append("category", category);
productData.append("quantity", quantity);
productData.append("brand", brand);
productData.append("countInStock", stock);
const { data } = await createProduct(productData);
if (data.error) {
toast.error("Product creation failed.");
} else {
toast.success(`${data.name} is created.`);
navigate("/");
}
} catch (error) {
console.error(error);
toast.error("Product creation failed.");
}
};
const uploadFileHandler = async (e) => {
const formData = new FormData();
formData.append("image", e.target.files[0]);
try {
const res = await useUploadProductImage(formData).unwrap();
toast.success(res.message);
setImage(res.image);
setImageUrl(res.image);
} catch (error) {
toast.error(error?.data?.message || error.error);
}
};
return (
<div className="container xl:mx-[9rem] sm:mx-[0]">
<div className="flex flex-col md:flex-row">
<AdminMenu />
<div className="md:w-3/4 p-3">
<div className="h-12">Create Product </div>
{imageUrl && (
<div className="text-center">
<img
src={imageUrl}
alt="product"
className="block mx-auto max-h-[200px]"
/>
</div>
)}
<div className="mb-3">
<label className="border border-black px-4 block w-full text-center rounded-lg cursor-pointer font-bold py-11">
{image ? image.name : "Upload Image"}
<input
type="file"
name="image"
accept="image/*"
onChange={uploadFileHandler}
className={!image ? "hidden" : "text-white"}
/>
</label>
</div>
<div className="p-3">
<div className="flex flex-wrap gap-5 justify-center">
<div className="one">
<label htmlFor="name">Name</label> <br />
<input
type="text"
className="p-4 mb-3 w-[30rem] border rounded-lg"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="two">
<label htmlFor="name block">Price</label> <br />
<input
type="number"
className="p-4 mb-3 w-[30rem] border rounded-lg"
value={price}
onChange={(e) => setPrice(e.target.value)}
/>
</div>
</div>
<div className="flex flex-wrap gap-5 justify-center">
<div className="one">
<label htmlFor="name block">Quantity</label> <br />
<input
type="number"
className="p-4 mb-3 w-[30rem] border rounded-lg"
value={quantity}
onChange={(e) => setQuantity(e.target.value)}
/>
</div>
<div className="two">
<label htmlFor="name block">Brand</label> <br />
<input
type="text"
className="p-4 mb-3 w-[30rem] border rounded-lg"
value={brand}
onChange={(e) => setBrand(e.target.value)}
/>
</div>
</div>
<label htmlFor="" className="my-5">
Description
</label>
<textarea
type="text"
className="p-2 mb-3 border rounded-lg w-[100%]"
value={description}
onChange={(e) => setDescription(e.target.value)}
></textarea>
<div className="flex justify-center gap-5">
<div>
<label htmlFor="name block">Count In Stock</label> <br />
<input
type="text"
className="p-4 mb-3 w-[34rem] border rounded-lg"
value={stock}
onChange={(e) => setStock(e.target.value)}
/>
</div>
<div>
<label htmlFor="">Category</label>
<select
placeholder="Choose Category"
className="p-4 mb-3 w-[34rem] border rounded-lg"
onChange={(e) => setCategory(e.target.value)}
>
<option>--Pick One--</option>
{categories?.map((c) => (
<option key={c._id} value={c._id}>
{c.name}
</option>
))}
</select>
</div>
</div>
<button
onClick={handleSubmit}
className="py-4 px-10 mt-5 rounded-lg text-lg fond-bold bg-pink-600"
>
Submit
</button>
</div>
</div>
</div>
</div>
);
};
export default ProductList;

@ -0,0 +1,261 @@
import { useState, useEffect } from "react";
import AdminMenu from "./AdminMenu";
import { useNavigate, useParams } from "react-router-dom";
import {
useUpdateProductMutation,
useDeleteProductMutation,
useGetProductByIdQuery,
useUploadProductImageMutation,
} from "../../redux/api/productApiSlice";
import { useFetchCategoriesQuery } from "../../redux/api/categoryApiSlice";
import { toast } from "react-toastify";
const AdminProductUpdate = () => {
const params = useParams();
const { data: productData } = useGetProductByIdQuery(params._id);
console.log(productData);
const [image, setImage] = useState(productData?.image || "");
const [name, setName] = useState(productData?.name || "");
const [description, setDescription] = useState(
productData?.description || ""
);
const [price, setPrice] = useState(productData?.price || "");
const [category, setCategory] = useState(productData?.category || "");
const [quantity, setQuantity] = useState(productData?.quantity || "");
const [brand, setBrand] = useState(productData?.brand || "");
const [stock, setStock] = useState(productData?.countInStock);
// hook
const navigate = useNavigate();
// Fetch categories using RTK Query
const { data: categories = [] } = useFetchCategoriesQuery();
const [uploadProductImage] = useUploadProductImageMutation();
// Define the update product mutation
const [updateProduct] = useUpdateProductMutation();
// Define the delete product mutation
const [deleteProduct] = useDeleteProductMutation();
useEffect(() => {
if (productData && productData._id) {
setName(productData.name);
setDescription(productData.description);
setPrice(productData.price);
setCategory(productData.category?._id);
setQuantity(productData.quantity);
setBrand(productData.brand);
setImage(productData.image);
}
}, [productData]);
const uploadFileHandler = async (e) => {
const formData = new FormData();
formData.append("image", e.target.files[0]);
try {
const res = await uploadProductImage(formData).unwrap();
toast.success("Item added successfully", {
position: toast.POSITION.TOP_RIGHT,
autoClose: 2000,
});
setImage(res.image);
} catch (err) {
toast.success("Item added successfully", {
position: toast.POSITION.TOP_RIGHT,
autoClose: 2000,
});
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const formData = new FormData();
formData.append("image", image);
formData.append("name", name);
formData.append("description", description);
formData.append("price", price);
formData.append("category", category);
formData.append("quantity", quantity);
formData.append("brand", brand);
formData.append("countInStock", stock);
// Update product using the RTK Query mutation
const data = await updateProduct({ productId: params._id, formData });
if (data?.error) {
toast.error(data.error);
} else {
toast.success(`Product successfully updated`);
navigate("/admin/allproductslist");
}
} catch (err) {
console.log(err);
toast.error("Product update failed. Try again.");
}
};
const handleDelete = async () => {
try {
let answer = window.confirm(
"Are you sure you want to delete this product?"
);
if (!answer) return;
const { data } = await deleteProduct(params._id);
toast.success(`"${data.name}" is deleted`, {
position: toast.POSITION.TOP_RIGHT,
autoClose: 2000,
});
navigate("/admin/allproductslist");
} catch (err) {
console.log(err);
toast.error("Delete failed. Try again.", {
position: toast.POSITION.TOP_RIGHT,
autoClose: 2000,
});
}
};
return (
<>
<div className="container xl:mx-[9rem] sm:mx-[0]">
<div className="flex flex-col md:flex-row">
<AdminMenu />
<div className="md:w-3/4 p-3">
<div className="h-12">Update / Delete Product</div>
{image && (
<div className="text-center">
<img
src={image}
alt="product"
className="block mx-auto w-full h-[40%]"
/>
</div>
)}
<div className="mb-3">
<label className=" px-4 block w-full text-center rounded-lg cursor-pointer font-bold py-11">
{image ? image.name : "Upload image"}
<input
type="file"
name="image"
accept="image/*"
onChange={uploadFileHandler}
className="text-white"
/>
</label>
</div>
<div className="p-3">
<div className="flex flex-wrap">
<div className="one">
<label htmlFor="name">Name</label> <br />
<input
type="text"
className="p-4 mb-3 w-[30rem] border rounded-lg mr-[5rem]"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="two">
<label htmlFor="name block">Price</label> <br />
<input
type="number"
className="p-4 mb-3 w-[30rem] border rounded-lg"
value={price}
onChange={(e) => setPrice(e.target.value)}
/>
</div>
</div>
<div className="flex flex-wrap">
<div>
<label htmlFor="name block">Quantity</label> <br />
<input
type="number"
min="1"
className="p-4 mb-3 w-[30rem] border rounded-lg mr-[5rem]"
value={quantity}
onChange={(e) => setQuantity(e.target.value)}
/>
</div>
<div>
<label htmlFor="name block">Brand</label> <br />
<input
type="text"
className="p-4 mb-3 w-[30rem] border rounded-lg"
value={brand}
onChange={(e) => setBrand(e.target.value)}
/>
</div>
</div>
<label htmlFor="" className="my-5">
Description
</label>
<textarea
type="text"
className="p-2 mb-3 border rounded-lg w-[95%]"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<div className="flex justify-between">
<div>
<label htmlFor="name block">Count In Stock</label> <br />
<input
type="text"
className="p-4 mb-3 w-[34rem] border rounded-lg"
value={stock}
onChange={(e) => setStock(e.target.value)}
/>
</div>
<div>
<label htmlFor="">Category</label> <br />
<select
placeholder="Choose Category"
className="p-4 mb-3 w-[30rem] border rounded-lg mr-[5rem]"
onChange={(e) => setCategory(e.target.value)}
>
<option>--Pick One--</option>
{categories?.map((c) => (
<option key={c._id} value={c._id}>
{c.name}
</option>
))}
</select>
</div>
</div>
<div className="">
<button
onClick={handleSubmit}
className="py-4 px-10 mt-5 rounded-lg text-lg font-bold bg-green-600 mr-6"
>
Update
</button>
<button
onClick={handleDelete}
className="py-4 px-10 mt-5 rounded-lg text-lg font-bold bg-pink-600"
>
Delete
</button>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default AdminProductUpdate;

@ -0,0 +1,175 @@
import { useEffect, useState } from "react";
import { FaTrash, FaEdit, FaCheck, FaTimes } from "react-icons/fa";
import Loader from "../../components/Loader";
import { toast } from "react-toastify";
import {
useGetUsersQuery,
useDeleteUserMutation,
useUpdateUserMutation,
} from "../../redux/api/usersApiSlice";
import Message from "../../components/Message";
import AdminMenu from "./AdminMenu";
const UserList = () => {
const { data: users, refetch, isLoading, error } = useGetUsersQuery();
const [deleteUser] = useDeleteUserMutation();
const [updateUser] = useUpdateUserMutation();
const [editableUserId, setEditableUserId] = useState(null);
const [editableUserName, setEditableUserName] = useState("");
const [editableUserEmail, setEditableUserEmail] = useState("");
useEffect(() => {
refetch();
}, [refetch]);
const deleteHandler = async (id) => {
if (window.confirm("Are you sure?")) {
try {
await deleteUser(id);
alert("User Deleted Successfully");
} catch (error) {
toast.error(error.data.message || error.error);
}
}
};
const toggleEdit = (id, username, email) => {
setEditableUserId(id);
setEditableUserName(username);
setEditableUserEmail(email);
};
const updateHandler = async (id) => {
try {
await updateUser({
userId: id,
username: editableUserName,
email: editableUserEmail,
});
setEditableUserId(null);
refetch();
} catch (error) {
toast.error(error.data.message || error.error);
}
};
return (
<div className="p-4">
<AdminMenu />
<h1 className="text-2xl font-semibold mb-4 text-center">Users</h1>
{isLoading ? (
<Loader />
) : error ? (
<Message variant="danger">
{error?.data?.message || error.message}
</Message>
) : (
<div className="flex flex-col md:flex-row">
<table className="w-full md:w-4/5 mx-auto">
<thead>
<tr>
<th className="px-4 py-2 text-left">ID</th>
<th className="px-4 py-2 text-left">NAME</th>
<th className="px-4 py-2 text-left">EMAIL</th>
<th className="px-4 py-2 text-left">ADMIN</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user._id}>
<td className="px-4 py-2">{user._id}</td>
<td className="px-4 py-2">
{editableUserId === user._id ? (
<div className="flex items-center">
<input
type="text"
value={editableUserName}
onChange={(e) => setEditableUserName(e.target.value)}
className="w-full p-2 border rounded-lg"
/>
<button
onClick={() => updateHandler(user._id)}
className="ml-2 bg-blue-500 text-white py-2 px-4 rounded-lg"
>
<FaCheck />
</button>
</div>
) : (
<div className="flex items-center">
{user.username}{" "}
<button
onClick={() =>
toggleEdit(user._id, user.username, user.email)
}
>
<FaEdit className="ml-[1rem]" />
</button>
</div>
)}
</td>
<td className="px-4 py-2">
{editableUserId === user._id ? (
<div className="flex items-center">
<input
type="text"
value={editableUserEmail}
onChange={(e) => setEditableUserEmail(e.target.value)}
className="w-full p-2 border rounded-lg"
/>
<button
onClick={() => updateHandler(user._id)}
className="ml-2 bg-blue-500 text-white py-2 px-4 rounded-lg"
>
<FaCheck />
</button>
</div>
) : (
<div className="flex items center">
<p>{user.email}</p>
<button
onClick={() =>
toggleEdit(user._id, user.username, user.email)
}
>
<FaEdit className="ml-[1rem]" />
</button>
</div>
)}
</td>
<td>
<td className="px-4 py-2">
{user.isAdmin ? (
<FaCheck style={{ color: "green" }} />
) : (
<FaTimes style={{ color: "red" }} />
)}
</td>
</td>
<td className="px-4 py-2">
{!user.isAdmin && (
<div className="flex">
<button
onClick={() => deleteHandler(user._id)}
className="bg-red-600 hover:bg-red-700 text-white font-bold py-4 px-4 rounded"
>
<FaTrash />
</button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
};
export default UserList;

@ -0,0 +1,103 @@
import { useState, useEffect } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { useLoginMutation } from "../../redux/api/usersApiSlice";
import { setCredientials } from "../../redux/features/auth/authSlice";
import { toast } from "react-toastify";
import Loader from "../../components/Loader";
import { FaTrophy } from "react-icons/fa";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const navigate = useNavigate();
const [login, { isLoading }] = useLoginMutation();
const { userInfo } = useSelector((state) => state.auth);
const { search } = useLocation();
const sp = new URLSearchParams(search);
const redirect = sp.get("redirect") || "/";
useEffect(() => {
if (userInfo) {
navigate(redirect);
}
}, [navigate, redirect, userInfo]);
const submitHandler = async (e) => {
e.preventDefault();
try {
const res = await login({ email, password }).unwrap();
console.log(res);
dispatch(setCredientials({ ...res }));
} catch (error) {
toast.error(error?.data?.message || error.message);
}
};
return (
<div className="flex items-center justify-center">
<section className="pl-[10rem] flex flex-wrap">
<div className="mr-[4rem] mt-[5rem]">
<h1 className="text-2xl font-semibold mb-4">Login</h1>
<form onSubmit={submitHandler} className="container w-[40rem]">
<div className="my-[2rem]">
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
type="email"
id="email"
className="mt-1 p-2 border border-slate-600 rounded w-full"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="my-[2rem]">
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
type="password"
id="password"
className="mt-1 p-2 border border-slate-600 rounded w-full"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button
disabled={isLoading}
type="submit"
className="bg-pink-500 text-white px-4 py-2 rounded cursor-pointer my-[1rem]"
>
{isLoading ? "Signing In..." : "Sign In"}
</button>
{isLoading && <Loader />}
</form>
<div className="mt-4">
<p>
Create Account?{" "}
<Link
to={redirect ? `/register?redirect=${redirect}` : "/register"}
className="text-pink-500 hover:underline"
>
Register
</Link>
</p>
</div>
</div>
</section>
</div>
);
};
export default Login;

@ -0,0 +1,36 @@
#navigation-container {
width: 4%;
transition: width 0.3s ease-in-out;
overflow: hidden; /* Hide overflowing content during transition */
}
#navigation-container:hover {
width: 15%;
}
/* Initially hide nav-item-name */
.nav-item-name {
display: none;
transition: opacity 0.2s ease-in-out; /* Add opacity transition */
}
#navigation-container:hover .nav-item-name {
display: block;
opacity: 1;
}
.search-input {
display: none;
opacity: 0;
transition: opacity 0.2s ease-in-out; /* Add opacity transition */
}
#navigation-container:hover .search-input {
display: block;
opacity: 1;
}
#navigation-container:hover .search-icon {
display: none;
transition: opacity 0.2s ease-in-out; /* Add opacity transition */
}

@ -0,0 +1,234 @@
import { useState } from "react";
import {
AiOutlineHome,
AiOutlineShopping,
AiOutlineLogin,
AiOutlineUserAdd,
AiOutlineShoppingCart,
} from "react-icons/ai";
import { FaHeart } from "react-icons/fa";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import "./Navigation.css";
import { useSelector, useDispatch } from "react-redux";
import { useLoginMutation, useLogoutMutation } from "../../redux/api/usersApiSlice";
import { logout } from "../../redux/features/auth/authSlice";
import FavoritesCount from "../Products/FavoritesCount";
const Navigation = () => {
const { userInfo } = useSelector((state) => state.auth);
const { cartItems } = useSelector((state) => state.cart);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [showSidebar, setShowSidebar] = useState(false);
const toggleDropdown = () => {
setDropdownOpen(!dropdownOpen);
};
const toggleSidebar = () => {
setShowSidebar(!showSidebar);
};
const closeSidebar = () => {
setShowSidebar(false);
};
const dispatch = useDispatch();
const navigate = useNavigate();
const [logoutApiCall] = useLogoutMutation();
const logoutHandler = async () => {
try {
await logoutApiCall().unwrap();
dispatch(logout());
navigate("/login");
} catch (error) {
console.log(error);
}
};
return (
<div
style={{ zIndex: 9999 }}
className={`${
showSidebar ? "hidden" : "flex"
} xl:flex lg:flex md:hidden sm:hidden flex-col justify-between p-4 text-white bg-[#000] w-[4%] hover:w-[15%] h-[100vh] fixed `}
id="navigation-container"
>
<div className="flex flex-col justify-center space-y-4">
<Link
to="/"
className="flex items-center transition-transform transform hover:translate-x-2"
>
<AiOutlineHome className="mr-2 mt-[3rem]" size={26} />
<span className="hidden nav-item-name mt-[3rem]">HOME</span>{" "}
</Link>
<Link
to="/shop"
className="flex items-center transition-transform transform hover:translate-x-2"
>
<AiOutlineShopping className="mr-2 mt-[3rem]" size={26} />
<span className="hidden nav-item-name mt-[3rem]">SHOP</span>{" "}
</Link>
<Link
to="/cart"
className="flex items-center transition-transform transform hover:translate-x-2"
>
<AiOutlineShoppingCart className="mr-2 mt-[3rem]" size={26} />
<span className="hidden nav-item-name mt-[3rem]">CART</span>{" "}
<div className="absolute top-9">
{cartItems.length > 0 && (
<span>
<span className="px-1 py-0 text-sm text-white bg-pink-500 rounded-full">
{cartItems.reduce((a, c) => a + c.qty, 0)}
</span>
</span>
)}
</div>
</Link>
<Link
to="/favorite"
className="flex items-center transition-transform transform hover:translate-x-2"
>
<FaHeart className="mr-2 mt-[3rem]" size={26} />
<span className="hidden nav-item-name mt-[3rem]">FAVORITE</span>{" "}
<FavoritesCount />
</Link>
</div>
<div className="relative">
<button
onClick={toggleDropdown}
className="flex items-center text-gray-8000 focus:outline-none"
>
{userInfo ? (
<span className="text-white">{userInfo.username}</span>
) : (
<></>
)}
{userInfo && (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`h-4 w-4 ml-1 ${
dropdownOpen ? "transform rotate-180" : ""
}`}
fill="none"
viewBox="0 0 24 24"
stroke="white"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d={dropdownOpen ? "M5 15l7-7 7 7" : "M19 9l-7 7-7-7"}
/>
</svg>
)}
</button>
{dropdownOpen && userInfo && (
<ul
className={`absolute right-0 mt-2 mr-14 space-y-2 bg-white text-gray-600 ${
!userInfo.isAdmin ? "-top-20" : "-top-80"
} `}
>
{userInfo.isAdmin && (
<>
<li>
<Link
to="/admin/dashboard"
className="block px-4 py-2 hover:bg-gray-100 hover:text-cyan-500"
>
Dashboard
</Link>
</li>
<li>
<Link
to="/admin/productlist"
className="block px-4 py-2 hover:bg-gray-100 hover:text-cyan-500"
>
Products
</Link>
</li>
<li>
<Link
to="/admin/categorylist"
className="block px-4 py-2 hover:bg-gray-100 hover:text-cyan-500"
>
Category
</Link>
</li>
<li>
<Link
to="/admin/orderlist"
className="block px-4 py-2 hover:bg-gray-100 hover:text-cyan-500"
>
Orders
</Link>
</li>
<li>
<Link
to="/admin/userlist"
className="block px-4 py-2 hover:bg-gray-100 hover:text-cyan-500"
>
Users
</Link>
</li>
</>
)}
<li>
<Link
to="/profile"
className="block px-4 py-2 hover:bg-gray-100 hover:text-cyan-500"
>
Profile
</Link>
</li>
<li>
<Link
onClick={logoutHandler}
className="block px-4 py-2 hover:bg-gray-100 hover:text-cyan-500"
>
Logout
</Link>
</li>
</ul>
)}
</div>
{!userInfo && (
<ul>
<li>
<Link
to="/login"
className="flex items-center transition-transform transform hover:translate-x-2"
>
<AiOutlineLogin className="mr-2 mt-[3rem]" size={26} />
<span className="hidden nav-item-name mt-[3rem]">Login</span>{" "}
</Link>
</li>
<li>
<Link
to="/register"
className="flex items-center transition-transform transform hover:translate-x-2"
>
<AiOutlineUserAdd className="mr-2 mt-[3rem]" size={26} />
<span className="hidden nav-item-name mt-[3rem]">
Register
</span>{" "}
</Link>
</li>
</ul>
)}
</div>
);
};
export default Navigation;

@ -0,0 +1,135 @@
import { useState, useEffect } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import Loader from "../../components/Loader";
import { setCredientials } from "../../redux/features/auth/authSlice";
import { toast } from "react-toastify";
import { useRegisterMutation } from "../../redux/api/usersApiSlice";
const Register = () => {
const [username, setUserName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const dispatch = useDispatch();
const navigate = useNavigate();
const [register, { isLoading }] = useRegisterMutation();
const { userInfo } = useSelector((state) => state.auth);
const { search } = useLocation();
const sp = new URLSearchParams(search);
const redirect = sp.get("redirect") || "/";
useEffect(() => {
if (userInfo) {
navigate(redirect);
}
}, [navigate, redirect, userInfo]);
const submitHandler = async (e) => {
e.preventDefault();
if (password !== confirmPassword) {
toast.error("Password and Confim Password do not match");
} else {
try {
const res = await register({ username, email, password }).unwrap();
dispatch(setCredientials({ ...res }));
navigate(redirect);
toast.success("User successfully registered");
} catch (error) {
console.log(error);
toast.error(error.data.message);
}
}
};
return (
<div className="flex items-center justify-center">
<section className="pl-[10rem] flex flex-wrap">
<div className="mr-[4rem] mt-[5rem]">
<h1 className="text-2xl font-semibold mb-4">Register</h1>
<form onSubmit={submitHandler} className="container w-[40rem]">
<div className="my-[2rem]">
<label htmlFor="name" className="block text-sm font-medium">
Name
</label>
<input
type="text"
id="name"
className="mt-1 p-2 border border-slate-600 rounded w-full"
value={username}
onChange={(e) => setUserName(e.target.value)}
/>
</div>
<div className="my-[2rem]">
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
type="email"
id="email"
className="mt-1 p-2 border border-slate-600 rounded w-full"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="my-[2rem]">
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
type="password"
id="password"
className="mt-1 p-2 border border-slate-600 rounded w-full"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="my-[2rem]">
<label
htmlFor="confirmPassword"
className="block text-sm font-medium"
>
Confirm Password
</label>
<input
type="password"
id="confirmPassword"
className="mt-1 p-2 border border-slate-600 rounded w-full"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<button
disabled={isLoading}
className="bg-pink-500 text-white px-4 py-2 rounded cursor-pointer my-[1rem]"
type="submit"
>
{isLoading ? "Registering..." : "Register"}
</button>
{isLoading && <Loader />}
</form>
<div className="mt-4">
<p>
Already have an account? {" "}
<Link
to={redirect ? `/login?redirect=${redirect}` : "/login"}
className="text-pink-500 hover:underline"
>
Login
</Link>
</p>
</div>
</div>
</section>
</div>
);
};
export default Register;

@ -0,0 +1,115 @@
import { Link, useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { FaTrash } from "react-icons/fa";
import { addToCart, removeFromCart } from "../redux/features/cart/cartSlice";
const Cart = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const cart = useSelector((state) => state.cart);
const { cartItems } = cart;
const addToCartHandler = (product, qty) => {
dispatch(addToCart({ ...product, qty }));
};
const removeFromCartHandler = (id) => {
dispatch(removeFromCart(id));
};
const checkoutHandler = () => {
navigate("/login?redirect=/shipping");
};
return (
<>
<div className="container flex justify-around items-start wrap mx-auto mt-8">
{cartItems.length === 0 ? (
<div>
Your cart is empty <Link to="/shop">Go To Shop</Link>
</div>
) : (
<>
<div className="flex flex-col w-[80%]">
<h1 className="text-2xl font-semibold mb-4">Shopping Cart</h1>
{cartItems.map((item) => (
<div key={item._id} className="flex items-enter mb-[1rem] pb-2">
<div className="w-[5rem] h-[5rem]">
<img
src={item.image}
alt={item.name}
className="w-full h-full object-cover rounded"
/>
</div>
<div className="flex-1 ml-4">
<Link to={`/product/${item._id}`} className="text-pink-500">
{item.name}
</Link>
<div className="mt-2">{item.brand}</div>
<div className="mt-2 font-bold">
$ {item.price}
</div>
</div>
<div className="w-24">
<select
className="w-full p-1 border rounded text-black"
value={item.qty}
onChange={(e) =>
addToCartHandler(item, Number(e.target.value))
}
>
{[...Array(item.countInStock).keys()].map((x) => (
<option key={x + 1} value={x + 1}>
{x + 1}
</option>
))}
</select>
</div>
<div>
<button
className="text-red-500 mr-[5rem]"
onClick={() => removeFromCartHandler(item._id)}
>
<FaTrash className="ml-[1rem] mt-[.5rem]" />
</button>
</div>
</div>
))}
<div className="mt-8 w-[40rem]">
<div className="p-4 rounded-lg">
<h2 className="text-xl font-semibold mb-2">
Items ({cartItems.reduce((acc, item) => acc + item.qty, 0)})
</h2>
<div className="text-2xl font-bold">
${" "}
{cartItems
.reduce((acc, item) => acc + item.qty * item.price, 0)
.toFixed(2)}
</div>
<button
className="bg-pink-500 mt-4 py-2 px-4 rounded-full text-lg w-full"
disabled={cartItems.length === 0}
onClick={checkoutHandler}
>
Proceed To Checkout
</button>
</div>
</div>
</div>
</>
)}
</div>
</>
);
};
export default Cart;

@ -0,0 +1,75 @@
import { useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import { PayPalButtons, usePayPalScriptReducer } from "@paypal/react-paypal-js";
import { useSelector } from "react-redux";
import { toast } from "react-toastify";
import Message from "../../components/Message";
import Loader from "../../components/Loader";
import {
useDeliverOrderMutation,
useGetOrderDetailsQuery,
useGetPaypalClientIdQuery,
usePayOrderMutation,
} from "../../redux/api/orderApiSlice";
const Order = () => {
const { id: orderId } = useParams();
const {
data: order,
refetch,
isLoading,
error,
} = useGetOrderDetailsQuery(orderId);
const [payOrder, { isLoading: loadingPay }] = usePayOrderMutation();
const [deliverOrder, { isLoading: loadingDeliver }] = useDeliverOrderMutation();
const { userInfo } = useSelector((state) => state.auth);
const [{ isPending }, paypalDispatch] = usePayPalScriptReducer();
const {
data: paypal,
isLoading: loadingPaPal,
error: errorPayPal,
} = useGetPaypalClientIdQuery();
useEffect(() => {
if (!errorPayPal && !loadingPaPal && paypal.clientId) {
const loadingPaPalScript = async () => {
paypalDispatch({
type: "resetOptions",
value: {
"client-id": paypal.clientId,
currency: "USD",
},
}),
paypalDispatch({ type: "setLoadingStatus", valie: "pending" });
};
if (order && !order.isPaid) {
if (!window.paypal) {
loadingPaPalScript();
}
}
}
}, [errorPayPal, loadingPaPal, order, paypal, paypalDispatch]);
return isLoading ? (
<Loader />
) : error ? (
<Message variant="danger">{error.data.message}</Message>
) : (
<div className="container flex flex-col ml-[10rem] md:flex-row">
<div className="md:w-2/3 pr-4">
<div className="border gray-300 mt-5 pb-4 mb-5">
{order.orderItems.length === 0 ? (
<Message> Order is Empty </Message>
) : (
<h1>Hello</h1>
)}
</div>
</div>
</div>
);
};
export default Order;

@ -0,0 +1,145 @@
import { useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { useDispatch, useSelector } from "react-redux";
import Message from "../../components/Message";
import ProgressSteps from "../../components/ProgressSteps";
import Loader from "../../components/Loader";
import { useCreateOrderMutation } from "../../redux/api/orderApiSlice";
import { clearCartItems } from "../../redux/features/cart/cartSlice";
const PlaceOrder = () => {
const navigate = useNavigate();
const cart = useSelector((state) => state.cart);
const [createOrder, { isLoading, error }] = useCreateOrderMutation();
useEffect(() => {
if (!cart.shippingAddress.address) {
navigate("/shipping");
}
}, [cart.paymentMethod, cart.shippingAddress.address, navigate]);
const dispatch = useDispatch();
const placeOrderHandler = async () => {
try {
const res = await createOrder({
orderItems: cart.cartItems,
shippingAddress: cart.shippingAddress,
paymentMethod: cart.paymentMethod,
itemsPrice: cart.itemsPrice,
shippingPrice: cart.shippingPrice,
taxPrice: cart.taxPrice,
totalPrice: cart.totalPrice,
}).unwrap();
dispatch(clearCartItems());
navigate(`/order/${res._id}`);
} catch (error) {
toast.error(error);
}
};
return (
<>
<ProgressSteps step1 step2 step3 />
<div className="container mx-auto mt-8 px-20 ">
{cart.cartItems.length === 0 ? (
<Message>Your cart is empty</Message>
) : (
<div className="overflow-x-auto mx-auto">
<table className="w-full border-collapse">
<thead>
<tr>
<td className="px-1 py-2 text-left align-top">Image</td>
<td className="px-1 py-2 text-left">Product</td>
<td className="px-1 py-2 text-left">Quantity</td>
<td className="px-1 py-2 text-left">Price</td>
<td className="px-1 py-2 text-left">Total</td>
</tr>
</thead>
<tbody>
{cart.cartItems.map((item, index) => (
<tr key={index}>
<td className="p-2">
<img
src={item.image}
alt={item.name}
className="w-16 h-16 object-cover"
/>
</td>
<td className="p-2">
<Link to={`/product/${item.product}`}>{item.name}</Link>
</td>
<td className="p-2">{item.qty}</td>
<td className="p-2">{item.price.toFixed(2)}</td>
<td className="p-2">
$ {(item.qty * item.price).toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<div className="mt-8">
<h2 className="text-2xl font-semibold mb-5">Order Summary</h2>
<div className="flex justify-between flex-wrap p-8">
<ul className="text-lg">
<li>
<span className="font-semibold mb-4">Items:</span> $
{cart.itemsPrice}
</li>
<li>
<span className="font-semibold mb-4">Shipping:</span> $
{cart.shippingPrice}
</li>
<li>
<span className="font-semibold mb-4">Tax:</span> $
{cart.taxPrice}
</li>
<li>
<span className="font-semibold mb-4">Total:</span> $
{cart.totalPrice}
</li>
</ul>
{error && <Message variant="danger">{error.data.message}</Message>}
<div>
<h2 className="text-2xl font-semibold mb-4">Shipping</h2>
<p>
<strong>Address:</strong> {cart.shippingAddress.address},{" "}
{cart.shippingAddress.city} {cart.shippingAddress.postalCode},{" "}
{cart.shippingAddress.country}
</p>
</div>
<div>
<h2 className="text-2xl font-semibold mb-4">Payment Method</h2>
<strong>Method:</strong> {cart.paymentMethod}
</div>
</div>
<button
type="button"
className="bg-pink-500 text-white py-2 px-4 rounded-full text-lg w-full mt-4"
disabled={cart.cartItems === 0}
onClick={placeOrderHandler}
>
Place Order
</button>
{isLoading && <Loader />}
</div>
</div>
</>
);
};
export default PlaceOrder;

@ -0,0 +1,120 @@
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import {
saveShippingAddress,
savePaymentMethod,
} from "../../redux/features/cart/cartSlice";
import ProgressSteps from "../../components/ProgressSteps.jsx";
const Shipping = () => {
const cart = useSelector((state) => state.cart);
const { shippingAddress } = cart;
const [paymentMethod, setPaymentMethod] = useState("PayPal");
const [address, setAddress] = useState(shippingAddress.address || "");
const [city, setCity] = useState(shippingAddress.city || "");
const [postalCode, setPostalCode] = useState(
shippingAddress.postalCode || ""
);
const [country, setCountry] = useState(shippingAddress.country || "");
const dispatch = useDispatch();
const navigate = useNavigate();
const submitHandler = (e) => {
e.preventDefault();
dispatch(saveShippingAddress({ address, city, postalCode, country }));
dispatch(savePaymentMethod(paymentMethod));
navigate("/placeorder");
};
// Payment
useEffect(() => {
if (!shippingAddress.address) {
navigate("/shipping");
}
}, [navigate, shippingAddress]);
return (
<div className="container mx-auto mt-10">
<ProgressSteps step1 step2 />
<div className="mt-[10rem] flex justify-around items-center flex-wrap">
<form onSubmit={submitHandler} className="w-[40rem]">
<h1 className="text-2xl font-semibold mb-4">Shipping</h1>
<div className="mb-4">
<label className="block mb-2">Address</label>
<input
type="text"
className="w-full p-2 border rounded"
placeholder="Enter address"
value={address}
required
onChange={(e) => setAddress(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block mb-2">City</label>
<input
type="text"
className="w-full p-2 border rounded"
placeholder="Enter city"
value={city}
required
onChange={(e) => setCity(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block mb-2">Postal Code</label>
<input
type="text"
className="w-full p-2 border rounded"
placeholder="Enter postal code"
value={postalCode}
required
onChange={(e) => setPostalCode(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block mb-2">Country</label>
<input
type="text"
className="w-full p-2 border rounded"
placeholder="Enter country"
value={country}
required
onChange={(e) => setCountry(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block text-gray-500">Select Method</label>
<div className="mt-2">
<label className="inline-flex items-center">
<input
type="radio"
className="form-radio text-pink-500"
name="paymentMethod"
value="PayPal"
checked={paymentMethod === "PayPal"}
onChange={(e) => setPaymentMethod(e.target.value)}
/>
<span className="ml-2">PayPal</span>
</label>
</div>
</div>
<button
className="bg-pink-500 text-white py-2 px-4 rounded-full text-lg w-full"
type="submit"
>
Continue
</button>
</form>
</div>
</div>
);
};
export default Shipping;

@ -0,0 +1,23 @@
import { useSelector } from "react-redux";
import { selectFavoriteProduct } from "../../redux/features/favorites/favoriteSlice";
import Product from "./Product";
const Favorites = () => {
const favorites = useSelector(selectFavoriteProduct);
return (
<div className="ml-[10rem]">
<h1 className="text-lg font-bold ml-[3rem] mt-[3rem]">
FAVORITE PRODUCTS
</h1>
<div className="flex flex-wrap">
{favorites.map((product) => (
<Product key={product._id} product={product} />
))}
</div>
</div>
);
};
export default Favorites;

@ -0,0 +1,18 @@
import { useSelector } from "react-redux/es/hooks/useSelector";
const FavoritesCount = () => {
const favorites = useSelector((state) => state.favorites);
const favoriteCount = favorites.length;
return (
<div className="absolute left-2 top-8">
{favoriteCount > 0 && (
<span className="px-1 py-0 text-sm text-white bg-pink-500 rounded-full">
{favoriteCount}
</span>
)}
</div>
);
};
export default FavoritesCount;

@ -0,0 +1,49 @@
import { FaHeart, FaRegHeart } from "react-icons/fa";
import { useSelector, useDispatch } from "react-redux";
import {
addToFavorites,
removeFromFavorites,
setFavorites,
} from "../../redux/features/favorites/favoriteSlice";
import {
addFavoritesToLocalStorage,
removeFavoritesFromLocalStorage,
getFavoritesFromLocalStorage,
} from "../../Utils/localStorage";
import { useEffect } from "react";
const HeartIcon = ({ product }) => {
const dispatch = useDispatch();
const favorites = useSelector((state) => state.favorites) || [];
const isFavorite = favorites.some((p) => p._id === product._id);
useEffect(() => {
const favoritesFromLocalStorage = getFavoritesFromLocalStorage();
dispatch(setFavorites(favoritesFromLocalStorage));
}, []);
const toggleFavorites = () => {
if(isFavorite){
dispatch(removeFromFavorites(product))
// remove the product from localstorage
removeFromFavorites(product._id)
} else {
dispatch(addToFavorites(product))
// add the product to localStorage
addFavoritesToLocalStorage(product)
}
}
return (
<div onClick={toggleFavorites} className="absolute top-2 right-5 cursor-pointer">
{isFavorite ? (
<FaHeart className="text-pink-500" />
) : (
<FaRegHeart className="text-white"/>
)}
</div>
);
};
export default HeartIcon;

@ -0,0 +1,30 @@
import { Link } from "react-router-dom";
import HeartIcon from "./HeartIcon";
const Product = ({ product }) => {
return (
<div className="w-[30rem] ml-[2rem] p-3 relative">
<div className="relative">
<img
src={product.image}
alt={product.name}
className="w-[30rem] rounded"
/>
<HeartIcon product={product} />
</div>
<div className="p-4">
<Link to={`/product/${product._id}`}>
<h2 className="flex justify-between items-center">
<div className="text-lg">{product.name}</div>
<span className="bg-pink-100 text-pink-800 text-sm font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-pink-900 dark:text-pink-300">
$ {product.price}
</span>
</h2>
</Link>
</div>
</div>
);
};
export default Product;

@ -0,0 +1,87 @@
import { Link } from "react-router-dom";
import { AiOutlineShoppingCart } from "react-icons/ai";
import { useDispatch } from "react-redux";
import { addToCart } from "../../redux/features/cart/cartSlice";
import { toast } from "react-toastify";
import HeartIcon from "./HeartIcon";
const ProductCard = ({ p }) => {
const dispatch = useDispatch();
const addToCartHandler = (product, qty) => {
dispatch(addToCart({ ...product, qty }));
toast.success("Item added successfully", {
position: toast.POSITION.TOP_RIGHT,
autoClose: 2000,
});
};
return (
<div className="max-w-sm relative rounded-lg shadow">
<section className="relative">
<Link to={`/product/${p._id}`}>
<span className="absolute bottom-3 right-3 bg-pink-100 text-pink-800 text-sm font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-pink-900 dark:text-pink-300">
{p?.brand}
</span>
<img
className="cursor-pointer w-full"
src={p.image}
alt={p.name}
style={{ height: "170px", objectFit: "cover" }}
/>
</Link>
<HeartIcon product={p} />
</section>
<div className="p-5">
<div className="flex justify-between">
<h5 className="mb-2 text-xl text-whiet dark:text-white">{p?.name}</h5>
<p className="font-semibold text-pink-500">
{p?.price?.toLocaleString("en-US", {
style: "currency",
currency: "USD",
})}
</p>
</div>
<p className="mb-3 font-normal">
{p?.description?.substring(0, 60)} ...
</p>
<section className="flex justify-between items-center">
<Link
to={`/product/${p._id}`}
className="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-pink-700 rounded-lg hover:bg-pink-800 focus:ring-4 focus:outline-none focus:ring-pink-300 dark:bg-pink-600 dark:hover:bg-pink-700 dark:focus:ring-pink-800"
>
Read More
<svg
className="w-3.5 h-3.5 ml-2"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M1 5h12m0 0L9 1m4 4L9 9"
/>
</svg>
</Link>
<button
className="p-2 rounded-full"
onClick={() => addToCartHandler(p, 1)}
>
<AiOutlineShoppingCart size={25} />
</button>
</section>
</div>
</div>
);
};
export default ProductCard;

@ -0,0 +1,108 @@
import { useGetTopProductQuery } from "../../redux/api/productApiSlice";
import Message from "../../components/Message";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import moment from "moment";
import {
FaBox,
FaClock,
FaShoppingCart,
FaStar,
FaStore,
} from "react-icons/fa";
const ProductCarousel = () => {
const { data: products, isLoading, error } = useGetTopProductQuery();
const settings = {
dots: false,
infinite: true,
speed: 500,
slidesToShow: 1,
slidesToScroll: 1,
arrows: true,
autoplay: true,
autoplaySpeed: 5000,
};
return (
<div className="mb-4 xl:block lg:block md:block">
{isLoading ? null : error ? (
<Message variant="danger">
{error?.data?.message || error.message}
</Message>
) : (
<Slider
{...settings}
className="xl:w-[50rem] lg:w-[50rem] md:-=[56rem] sm:w-[40rem] sm:block"
>
{products.map(
({
image,
_id,
name,
price,
description,
brand,
createdAt,
numReviews,
rating,
quantity,
countInStock,
}) => (
<div key={_id}>
<img
src={image}
alt={name}
className="w-full rounded-lg object-cover h-[30rem]"
/>
<div className="flex justify-between">
<div className="one">
<h2>{name}</h2>
<p>$ {price}</p>
<p className="pt-2 w-25rem">
{description.substring(0, 170)}...
</p>
</div>
<div className="flex justify-between">
<div className="one">
<h1 className="flex items-center mb-6 w-[12rem]">
<FaStore className="mr-2" /> Brand: {brand}
</h1>
<h1 className="flex items-center mb-6 w-[12rem]">
<FaClock className="mr-2" /> Added:{" "}
{moment(createdAt).fromNow()}
</h1>
<h1 className="flex items-center mb-6 w-[12rem]">
<FaStar className="mr-2" /> Reviews: {numReviews}
</h1>
</div>
<div className="two">
<h1 className="flex items-center mb-6 w-[10rem]">
<FaStar className="mr-2" /> Ratings:{" "}
{Math.round(rating)}
</h1>
<h1 className="flex items-center mb-6 w-[10rem]">
<FaShoppingCart className="mr-2" /> Quantity:{" "}
{quantity}
</h1>
<h1 className="flex items-center mb-6 w-[10rem]">
<FaBox className="mr-2" /> In Stock:{" "}
{countInStock}
</h1>
</div>
</div>
</div>
</div>
)
)}
</Slider>
)}
</div>
);
};
export default ProductCarousel;

@ -0,0 +1,188 @@
import { useState } from "react";
import { useParams, Link, useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { toast } from "react-toastify";
import {
useGetProductDetailsQuery,
useCreateReviewMutation,
} from "../../redux/api/productApiSlice";
import Loader from "../../components/Loader";
import Message from "../../components/Message";
import {
FaBox,
FaClock,
FaShoppingCart,
FaStar,
FaStore,
} from "react-icons/fa";
import moment from "moment";
import HeartIcon from "./HeartIcon";
import Ratings from "./Ratings.jsx";
import ProductTabs from "./ProductTabs";
import { addToCart } from "../../redux/features/cart/cartSlice";
const ProductDetails = () => {
const { id: productId } = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const [qty, setQty] = useState(1);
const [rating, setRating] = useState(0);
const [comment, setComment] = useState("");
const {
data: product,
isLoading,
refetch,
error,
} = useGetProductDetailsQuery(productId);
const { userInfo } = useSelector((state) => state.auth);
const [createReview, { isLoading: loadingProductReview }] =
useCreateReviewMutation();
const submitHandler = async (e) => {
e.preventDefault();
try {
await createReview({
productId,
rating,
comment,
}).unwrap();
refetch();
toast.success("Review created successfully");
} catch (error) {
toast.error(error?.data || error.message);
}
};
const addToCartHandler = () => {
dispatch(addToCart({ ...product, qty }));
navigate("/cart");
};
return (
<>
<div>
<Link
to="/"
className="font-semibold hover:underline ml-[10rem]"
>
Go Back
</Link>
</div>
{isLoading ? (
<Loader />
) : error ? (
<Message variant="danger">
{error?.data?.message || error.message}
</Message>
) : (
<>
<div className="flex flex-wrap relative items-between mt-[2rem] ml-[10rem]">
<div>
<img
src={product.image}
alt={product.name}
className="w-full xl:w-[50rem] lg:w-[45rem] md:w-[30rem] sm:w-[20rem] mr-[2rem]"
/>
<HeartIcon product={product} />
</div>
<div className="flex flex-col justify-between">
<h2 className="text-2xl font-semibold">{product.name}</h2>
<p className="my-4 xl:w-[35rem] lg:w-[35rem] md:w-[30rem] text-[#B0B0B0]">
{product.description}
</p>
<p className="text-5xl my-4 font-extrabold">$ {product.price}</p>
<div className="flex items-center justify-between w-[20rem]">
<div className="one">
<h1 className="flex items-center mb-6">
<FaStore className="mr-2" /> Brand:{" "}
{product.brand}
</h1>
<h1 className="flex items-center mb-6 w-[20rem]">
<FaClock className="mr-2" /> Added:{" "}
{moment(product.createAt).fromNow()}
</h1>
<h1 className="flex items-center mb-6">
<FaStar className="mr-2" /> Reviews:{" "}
{product.numReviews}
</h1>
</div>
<div className="two">
<h1 className="flex items-center mb-6">
<FaStar className="mr-2" /> Ratings: {rating}
</h1>
<h1 className="flex items-center mb-6">
<FaShoppingCart className="mr-2" /> Quantity:{" "}
{product.quantity}
</h1>
<h1 className="flex items-center mb-6 w-[10rem]">
<FaBox className="mr-2" /> In Stock:{" "}
{product.countInStock}
</h1>
</div>
</div>
<div className="flex justify-between flex-wrap">
<Ratings
value={product.rating}
text={`${product.numReviews} reviews`}
/>
{product.countInStock > 0 && (
<div>
<select
value={qty}
onChange={(e) => setQty(e.target.value)}
className="p-2 w-[6rem] rounded-lg text-black"
>
{[...Array(product.countInStock).keys()].map((x) => (
<option key={x + 1} value={x + 1}>
{x + 1}
</option>
))}
</select>
</div>
)}
</div>
<div className="btn-container">
<button
onClick={addToCartHandler}
disabled={product.countInStock === 0}
className="bg-pink-600 text-white py-2 px-4 rounded-lg mt-4 md:mt-0"
>
Add To Cart
</button>
</div>
</div>
<div className="mt-[5rem] container flex flex-wrap items-start justify-between ml-[10rem]">
<ProductTabs
loadingProductReview={loadingProductReview}
userInfo={userInfo}
submitHandler={submitHandler}
rating={rating}
setRating={setRating}
comment={comment}
setComment={setComment}
product={product}
/>
</div>
</div>
</>
)}
</>
);
};
export default ProductDetails;

@ -0,0 +1,163 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import Ratings from "./Ratings";
import { useGetTopProductQuery } from "../../redux/api/productApiSlice";
import SmallProduct from "./SmallProduct";
import Loader from "../../components/Loader";
const ProductTabs = ({
loadingProductReview,
userInfo,
submitHandler,
rating,
setRating,
comment,
setComment,
product,
}) => {
const { data, isLoading } = useGetTopProductQuery();
const [activeTab, setActiveTab] = useState(1);
if (isLoading) {
return <Loader />;
}
const handleTabClick = (tabNumber) => {
setActiveTab(tabNumber);
};
return (
<div className="flex flex-col md:flex-row">
<section className="mr-[5rem]">
<div
className={`flex-1 p-4 cursor-pointer text-lg ${
activeTab === 1 ? "font-bold" : ""
}`}
onClick={() => handleTabClick(1)}
>
Write Your Review
</div>
<div
className={`flex-1 p-4 cursor-pointer text-lg ${
activeTab === 2 ? "font-bold" : ""
}`}
onClick={() => handleTabClick(2)}
>
All Reviews
</div>
<div
className={`flex-1 p-4 cursor-pointer text-lg ${
activeTab === 3 ? "font-bold" : ""
}`}
onClick={() => handleTabClick(3)}
>
Related Products
</div>
</section>
{/* Second Part */}
<section>
{activeTab === 1 && (
<div className="mt-4">
{userInfo ? (
<form onSubmit={submitHandler}>
<div className="my-2">
<label htmlFor="rating" className="block text-xl mb-2">
Rating
</label>
<select
id="rating"
required
value={rating}
onChange={(e) => setRating(e.target.value)}
className="p-2 border rounded-lg xl:w-[40rem] text-black"
>
<option value="">Select</option>
<option value="1">Inferior</option>
<option value="2">Decent</option>
<option value="3">Great</option>
<option value="4">Excellent</option>
<option value="5">Exceptional</option>
</select>
</div>
<div className="my-2">
<label htmlFor="comment" className="block text-xl mb-2">
Comment
</label>
<textarea
id="comment"
rows="3"
required
value={comment}
onChange={(e) => setComment(e.target.value)}
className="p-2 border rounded-lg xl:w-[40rem] text-black"
></textarea>
</div>
<button
type="submit"
disabled={loadingProductReview}
className="bg-pink-600 text-white py-2 px-4 rounded-lg"
>
Submit
</button>
</form>
) : (
<p>
Please <Link to="/login">sign in</Link> to write a review
</p>
)}
</div>
)}
</section>
<section>
{activeTab === 2 && (
<>
<div>{product.reviews.length === 0 && <p>No Reviews</p>}</div>
<div>
{product.reviews.map((review) => (
<div
key={review._id}
className="bg-gray-100 p-4 rounded-lg xl:ml-[2rem] sm:ml-[0rem] xl:w-[50rem] sm:w-[24rem] mb-5"
>
<div className="flex justify-between">
<strong className="text-[#B0B0B0]">{review.name}</strong>
<p className="text-[#B0B0B0]">
{review.createdAt.substring(0, 10)}
</p>
</div>
<p className="my-4">{review.comment}</p>
<Ratings value={review.rating} />
</div>
))}
</div>
</>
)}
</section>
<section>
{activeTab === 3 && (
<section className="ml-[4rem] flex flex-wrap">
{!data ? (
<Loader />
) : (
data.map((product) => (
<div key={product._id}>
<SmallProduct product={product} />
</div>
))
)}
</section>
)}
</section>
</div>
);
};
export default ProductTabs;

@ -0,0 +1,30 @@
import { FaRegStar, FaStar, FaStarHalfAlt } from "react-icons/fa";
const Ratings = ({ value, text, color }) => {
const fullStars = Math.floor(value);
const halfStars = value - fullStars > 0.5 ? 1 : 0;
const emptyStar = 5 - fullStars - halfStars;
return (
<div className="flex items-center">
{[...Array(fullStars)].map((_, index) => (
<FaStar key={index} className={`text-${color} ml-1`} />
))}
{halfStars === 1 && <FaStarHalfAlt className={`text-${color} ml-1`} />}
{[...Array(emptyStar)].map((_, index) => (
<FaRegStar key={index} className={`text-${color} ml-1`} />
))}
<span className={`rating-text ml-{2rem} text-${color}`}>
{text && text}
</span>
</div>
);
};
Ratings.defaultProps = {
color: "yellow-500",
};
export default Ratings;

@ -0,0 +1,32 @@
import { Link } from "react-router-dom";
import HeartIcon from './HeartIcon'
const SmallProduct = ({ product }) => {
return (
<div className="w-[20rem] ml-[2rem] p-3">
<div className="relative">
<img
src={product.image}
alt={product.name}
className="h-auto rounded"
/>
<HeartIcon product={product} />
<div className="p-54">
<Link to={`/product/${product._id}`}>
<h2 className="flex justify-between items-center ">
<div>
{product.name}
<span className="bg-pink-100 text-pink-800 text-sm font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-pink-900 dark:text-pink-300">
${product.price}
</span>
</div>
</h2>
</Link>
</div>
</div>
</div>
);
};
export default SmallProduct

@ -0,0 +1,185 @@
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useGetFilteredProductsQuery } from "../redux/api/productApiSlice";
import { useFetchCategoriesQuery } from "../redux/api/categoryApiSlice";
import {
setCategories,
setProducts,
setChecked,
} from "../redux/features/shop/shopSlice";
import Loader from "../components/Loader";
import ProductCard from "./Products/ProductCard.jsx";
const Shop = () => {
const dispatch = useDispatch();
const { categories, products, checked, radio } = useSelector(
(state) => state.shop
);
const categoriesQuery = useFetchCategoriesQuery();
const [priceFilter, setPriceFilter] = useState("");
const filteredProductsQuery = useGetFilteredProductsQuery({
checked,
radio,
});
useEffect(() => {
if (!categoriesQuery.isLoading) {
dispatch(setCategories(categoriesQuery.data));
}
}, [categoriesQuery.data, dispatch]);
useEffect(() => {
if (!checked.length || !radio.length) {
if (!filteredProductsQuery.isLoading) {
// Filter products based on both checked categories and price filter
const filteredProducts = filteredProductsQuery.data.filter(
(product) => {
// Check if the product price includes the entered price filter value
return (
product.price.toString().includes(priceFilter) ||
product.price === parseInt(priceFilter, 10)
);
}
);
dispatch(setProducts(filteredProducts));
}
}
}, [checked, radio, filteredProductsQuery.data, dispatch, priceFilter]);
const handleBrandClick = (brand) => {
const productsByBrand = filteredProductsQuery.data?.filter(
(product) => product.brand === brand
);
dispatch(setProducts(productsByBrand));
};
const handleCheck = (value, id) => {
const updatedChecked = value
? [...checked, id]
: checked.filter((c) => c !== id);
dispatch(setChecked(updatedChecked));
};
// Add "All Brands" option to uniqueBrands
const uniqueBrands = [
...Array.from(
new Set(
filteredProductsQuery.data
?.map((product) => product.brand)
.filter((brand) => brand !== undefined)
)
),
];
const handlePriceChange = (e) => {
// Update the price filter state when the user types in the input filed
setPriceFilter(e.target.value);
};
return (
<>
<div className="container mx-auto">
<div className="flex md:flex-row">
<div className="p-3 mt-2 mb-2">
<h2 className="h4 text-center py-2 bg-gray-300 rounded-full mb-2">
Filter by Categories
</h2>
<div className="p-5 w-[15rem]">
{categories?.map((c) => (
<div key={c._id} className="mb-2">
<div className="flex ietms-center mr-4">
<input
type="checkbox"
id="red-checkbox"
onChange={(e) => handleCheck(e.target.checked, c._id)}
className="w-4 h-4 text-pink-600 bg-gray-100 border-gray-300 rounded focus:ring-pink-500 dark:focus:ring-pink-600 dark:ring-offset-gray-800 focus:ring-2 dark:border-gray-600"
/>
<label
htmlFor="pink-checkbox"
className="ml-2 text-sm font-medium dark:text-gray-300"
>
{c.name}
</label>
</div>
</div>
))}
</div>
<h2 className="h4 text-center py-2 bg-gray-300 rounded-full mb-2">
Filter by Brands
</h2>
<div className="p-5">
{uniqueBrands?.map((brand) => (
<>
<div className="flex items-enter mr-4 mb-5">
<input
type="radio"
id={brand}
name="brand"
onChange={() => handleBrandClick(brand)}
className="w-4 h-4 text-pink-400 bg-gray-100 border-gray-300 focus:ring-pink-500 dark:focus:ring-pink-600 dark:ring-offset-gray-800 focus:ring-2 dark:border-gray-600"
/>
<label
htmlFor="pink-radio"
className="ml-2 text-sm font-medium dark:text-gray-300"
>
{brand}
</label>
</div>
</>
))}
</div>
<h2 className="h4 text-center py-2 bg-gray-300 rounded-full mb-2">
Filer by Price
</h2>
<div className="p-5 w-[15rem]">
<input
type="text"
placeholder="Enter Price"
value={priceFilter}
onChange={handlePriceChange}
className="w-full px-3 py-2 placeholder-gray-400 border rounded-lg focus:outline-none focus:ring focus:border-pink-300"
/>
</div>
<div className="p-5 pt-0">
<button
className="w-full border my-4"
onClick={() => window.location.reload()}
>
Reset
</button>
</div>
</div>
<div className="p-3">
<h2 className="h4 text-center mb-2">{products?.length} Products</h2>
<div className="flex flex-wrap">
{products.length === 0 ? (
<Loader />
) : (
products?.map((p) => (
<div className="p-3" key={p._id}>
<ProductCard p={p} />
</div>
))
)}
</div>
</div>
</div>
</div>
</>
);
};
export default Shop;

@ -0,0 +1,118 @@
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { toast } from "react-toastify";
import Loader from "../../components/Loader";
import { setCredientials } from "../../redux/features/auth/authSlice";
import { Link } from "react-router-dom";
import { useProfileMutation } from "../../redux/api/usersApiSlice";
const Profile = () => {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const { userInfo } = useSelector((state) => state.auth);
const [updateProfile, { isLoading: loadingUpdateProfile }] =
useProfileMutation();
useEffect(() => {
setUsername(userInfo.username);
setEmail(userInfo.email);
}, [userInfo.username, userInfo.email, userInfo]);
const dispatch = useDispatch();
const submitHandler = async (e) => {
e.preventDefault();
if (password !== confirmPassword) {
toast.error("Password are not match! try Again");
} else {
try {
const res = await updateProfile({
_id: userInfo._id,
username,
email,
password,
}).unwrap();
dispatch(setCredientials({ ...res }));
toast.success("Profile updated successfully");
} catch (error) {
toast.error(error?.data?.message || error.message);
}
}
};
return (
<div className="container mx-auto p-4 mt-[10rem]">
<div className="flex justify-center align-center md:flex md:space-x-4">
<div className="md:w-1/3">
<h2 className="text-2xl font-semibold mb-4">Update Profile</h2>
<form onSubmit={submitHandler}>
<div className="mb-4">
<label className="block mb-2">Name</label>
<input
type="text"
placeholder="Enter name"
className="form-input p-4 rounded-sm border border-slate-600 w-full"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block mb-2">Email</label>
<input
type="email"
placeholder="Enter email"
className="form-input p-4 rounded-sm border border-slate-600 w-full"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block mb-2">Password</label>
<input
type="password"
placeholder="Enter password"
className="form-input p-4 rounded-sm border border-slate-600 w-full"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="block mb-2">Confirm Password</label>
<input
type="password"
placeholder="Confirm Password"
className="form-input p-4 rounded-sm border border-slate-600 w-full"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<div className="flex justify-between">
<button
type="submit"
className="bg-pink-500 text-white py-2 px-4 rounder hover:bg-pink-600"
>
Update
</button>
<Link
to="/user-orders"
className="bg-pink-600 text-white py-2 px-4 rounded hover:bg-pink-700"
>
My Orders
</Link>
</div>
</form>
</div>
{loadingUpdateProfile && <Loader />}
</div>
</div>
);
};
export default Profile;

@ -0,0 +1,77 @@
import Message from "../../components/Message";
import Loader from "../../components/Loader";
import { Link } from "react-router-dom";
import { useGetMyOrdersQuery } from "../../redux/api/orderApiSlice";
const UserOrder = () => {
const { data: orders, isLoading, error } = useGetMyOrdersQuery();
console.log(orders)
return (
<div className="container mx-auto">
<h2 className="text-2xl font-semibold mb-4">My Orders</h2>
{isLoading ? (
<Loader />
) : error ? (
<Message variant="danger">{error?.data?.error || error.error}</Message>
) : (
<table className="w-full">
<thead>
<tr>
<td className="py-2">IMAGE</td>
<td className="py-2">ID</td>
<td className="py-2">DATE</td>
<td className="py-2">TOTAL</td>
<td className="py-2">PAID</td>
<td className="py-2">DELIVERED</td>
<td className="py-2"></td>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order._id}>
<img
src={order.orderItems[0].image}
alt={order.user}
className="w-[6rem] mb-5"
/>
<td className="py-2">{order._id}</td>
<td className="py-2">{order.createdAt.substring(0, 10)}</td>
<td className="py-2">$ {order.totalPrice}</td>
<td className="py-2">{order.isPaid ? (
<p className="p-1 text-center bg-green-400 w-[6rem] rounded-full">
Completed
</p>
): (
<p className="p-1 text-center bg-red-400 w-[6rem] rounded-full">
Pending
</p>
)}</td>
<td className="py-2 px-2">{order.isDelivered ? (
<p className="p-1 text-center bg-green-400 w-[6rem] rounded-full">
Completed
</p>
): (
<p className="p-1 text-center bg-red-400 w-[6rem] rounded-full">
Pending
</p>
)}</td>
<td className="px-2 py-2">
<Link to={`/order/${order._id}`}>
<button className="bg-pink-400 text-black py-2 px-3 rounded">
View Details
</button>
</Link>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
export default UserOrder;

@ -0,0 +1,19 @@
// Importing necessary functions from the Redux Toolkit for API handling
import { fetchBaseQuery, createApi } from "@reduxjs/toolkit/query/react";
// Importing the base URL from constants for API requests
import { BASE_URL } from "../constants";
// Creating a base query using the fetchBaseQuery function with the specified base URL
const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL });
// Creating an API slice using createApi with the configured base query and tag types
export const apiSlice = createApi({
baseQuery, // Configuring the API with the previously defined base query
// Defining tag types for better organization and documentation of entities (Product, Order, User, Category)
tagTypes: ["Product", "Order", "User", "Category"],
// Defining endpoints (empty for now, to be extended as needed)
endpoints: () => ({}),
});

@ -0,0 +1,40 @@
import { apiSlice } from "./apiSlice";
import { CATEGORY_URL } from "../constants";
export const categoryApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
createCategory: builder.mutation({
query: (newCategory) => ({
url: `${CATEGORY_URL}`,
method: 'POST',
body: newCategory,
})
}),
updateCategory: builder.mutation({
query: ({categoryId, updatedCategory}) => ({
url: `${CATEGORY_URL}/${categoryId}`,
method: 'PUT',
body: updatedCategory,
})
}),
deleteCategory: builder.mutation({
query: (categoryId) => ({
url: `${CATEGORY_URL}/${categoryId}`,
method: 'DELETE',
})
}),
fetchCategories: builder.query({
query: () => `${CATEGORY_URL}/categories`,
})
})
})
export const {
useCreateCategoryMutation,
useUpdateCategoryMutation,
useDeleteCategoryMutation,
useFetchCategoriesQuery
} = categoryApiSlice

@ -0,0 +1,80 @@
import { apiSlice } from "./apiSlice";
import { ORDERS_URL, PAYPAL_URL } from "../constants";
export const orderApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
createOrder: builder.mutation({
query: (order) => ({
url: ORDERS_URL,
method: "POST",
body: order,
}),
}),
getOrderDetails: builder.query({
query: (id) => ({
url: `${ORDERS_URL}/${id}`,
}),
}),
payOrder: builder.mutation({
query: ({ orderId, details }) => ({
url: `${ORDERS_URL}/${orderId}/pay`,
method: "PUT",
body: details,
}),
}),
getPaypalClientId: builder.query({
query: () => ({
url: PAYPAL_URL,
}),
}),
getMyOrders: builder.query({
query: () => ({
url: `${ORDERS_URL}/mine`,
}),
keepUnusedDataFor: 5,
}),
getOrders: builder.query({
query: () => ({
url: ORDERS_URL,
}),
}),
deliverOrder: builder.mutation({
query: (orderId) => ({
url: `${ORDERS_URL}/${orderId}/deliver`,
method: "PUT",
}),
}),
getTotalOrders: builder.query({
query: () => `${ORDERS_URL}/total-orders`,
}),
getTotalSales: builder.query({
query: () => `${ORDERS_URL}/total-sales`,
}),
getTotalSalesByDate: builder.query({
query: () => `${ORDERS_URL}/total-sales-by-date`,
}),
}),
});
export const {
useGetTotalOrdersQuery,
useGetTotalSalesQuery,
useGetTotalSalesByDateQuery,
// ------------------
useCreateOrderMutation,
useGetOrderDetailsQuery,
usePayOrderMutation,
useGetPaypalClientIdQuery,
useGetMyOrdersQuery,
useDeliverOrderMutation,
useGetOrdersQuery,
} = orderApiSlice;

@ -0,0 +1,107 @@
import { PRODUCT_URL, UPLOAD_URL } from "../constants";
import { apiSlice } from "./apiSlice";
export const productApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getProducts: builder.query({
query: ({ keyword }) => ({
url: `${PRODUCT_URL}`,
params: { keyword },
}),
keepUnusedDataFor: 5,
providesTags: ["Product"],
}),
getProductById: builder.query({
query: (productId) => `${PRODUCT_URL}/${productId}`,
providesTags: (result, error, productId) => [
{ types: "Product", id: productId },
],
}),
allProducts: builder.query({
query: () => `${PRODUCT_URL}/fetchAllProducts`,
}),
getProductDetails: builder.query({
query: (productId) => ({
url: `${PRODUCT_URL}/${productId}`,
}),
keepUnusedDataFor: 5,
}),
createProduct: builder.mutation({
query: (productData) => ({
url: `${PRODUCT_URL}`,
method: "POST",
body: productData,
}),
invalidatesTags: ["Product"],
}),
updateProduct: builder.mutation({
query: ({ productId, formData }) => ({
url: `${PRODUCT_URL}/${productId}`,
method: "PUT",
body: formData,
}),
}),
uploadProductImage: builder.mutation({
query: (data) => ({
url: `${UPLOAD_URL}`,
method: "POST",
body: data,
}),
}),
deleteProduct: builder.mutation({
query: (productId) => ({
url: `${PRODUCT_URL}/${productId}`,
method: "DELETE",
}),
providesTags: ["Product"],
}),
createReview: builder.mutation({
query: (data) => ({
url: `${PRODUCT_URL}/${data.productId}/reviews`,
method: "POST",
body: data,
}),
}),
getTopProduct: builder.query({
query: () => `${PRODUCT_URL}/top`,
keepUnusedDataFor: 5,
}),
getNewProduct: builder.query({
query: () => `${PRODUCT_URL}/new`,
keepUnusedDataFor: 5,
}),
getFilteredProducts: builder.query({
query: ({ checked, radio }) => ({
url: `${PRODUCT_URL}/filtered-products`,
method: "POST",
body: { checked, radio },
}),
}),
}),
});
export const {
useGetProductsQuery,
useGetProductByIdQuery,
useAllProductsQuery,
useGetProductDetailsQuery,
useCreateProductMutation,
useUpdateProductMutation,
useDeleteProductMutation,
useCreateReviewMutation,
useGetTopProductQuery,
useGetNewProductQuery,
useUploadProductImageMutation,
useGetFilteredProductsQuery,
} = productApiSlice;

@ -0,0 +1,86 @@
// Importing the apiSlice to extend and create a user-specific API slice
import { apiSlice } from "./apiSlice";
// Importing the constant for the users' URL from the constants file
import { USERS_URL } from "../constants";
// Creating a user-specific API slice by injecting endpoints into the base apiSlice
export const userApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
// Defining a login endpoint as a mutation with specified query details
login: builder.mutation({
query: (data) => ({
url: `${USERS_URL}/login`, // Constructing the URL for the login endpoint
method: "POST", // Setting the HTTP method to POST for login
body: data, // Including the request data in the body
}),
}),
logout: builder.mutation({
query: () => ({
url: `${USERS_URL}/logout`,
method: "POST",
}),
}),
register: builder.mutation({
query: (data) => ({
url: `${USERS_URL}/register`,
method: "POST",
body: data,
}),
}),
profile: builder.mutation({
query: (data) => ({
url: `${USERS_URL}/profile`,
method: "PUT",
body: data,
}),
}),
getUsers: builder.query({
query: () => ({
url: `${USERS_URL}/getAllUsers`,
}),
providesTags: ["User"],
keepUnusedDataFor: 5,
}),
deleteUser: builder.mutation({
query: (userId) => ({
url: `${USERS_URL}/${userId}`,
method: "DELETE",
}),
}),
getUserDetails: builder.query({
query: (id) => ({
url: `${USERS_URL}/${id}`,
}),
keepUnusedDataFor: 5,
}),
updateUser: builder.mutation({
query: (data) => ({
url: `${USERS_URL}/${data.userId}`,
method: "PUT",
body: data,
}),
invalidatesTags: ["User"],
}),
}),
});
// Extracting the generated hook for the login mutation from the userApiSlice
export const {
useLoginMutation,
useLogoutMutation,
useRegisterMutation,
useProfileMutation,
useGetUsersQuery,
useDeleteUserMutation,
useGetUserDetailsQuery,
useUpdateUserMutation
} = userApiSlice;

@ -0,0 +1,7 @@
export const BASE_URL = " "; //proxy
export const USERS_URL = "/b6/users";
export const CATEGORY_URL = '/b6/category'
export const PRODUCT_URL = '/b6/products'
export const UPLOAD_URL = '/b6/upload'
export const ORDERS_URL = '/b6/orders'
export const PAYPAL_URL = '/b6/config/paypal'

@ -0,0 +1,38 @@
// Importing createSlice function from Redux Toolkit for creating a slice of the Redux store
import { createSlice } from "@reduxjs/toolkit";
// Initializing the initial state for the auth slice, retrieving user info from localStorage if available
const initialState = {
userInfo: localStorage.getItem("userInfo")
? JSON.parse(localStorage.getItem("userInfo"))
: null,
};
// Creating the authSlice with a name, initial state, and reducer functions
const authSlice = createSlice({
name: "auth", // Name of the slice for identifying in the Redux store
initialState, // Initial state of the auth slice
// Reducer functions to handle state changes
reducers: {
// Reducer for setting user credentials in the state and localStorage
setCredientials: (state, action) => {
state.userInfo = action.payload; // Updating user info in the state
localStorage.setItem("userInfo", JSON.stringify(action.payload)); // Storing user info in localStorage
const expirationTime = new Date().getTime() + 30 * 24 * 60 * 60 * 1000; // Setting expiration time (e.g., 30 days)
localStorage.setItem("expirationTime", expirationTime); // Storing expiration time in localStorage
},
// Reducer for logging out, clearing user info from the state and localStorage
logout: (state) => {
state.userInfo = null; // Clearing user info in the state
localStorage.clear(); // Clearing all items in localStorage
},
},
});
// Extracting action creators for the setCredentials and logout reducers
export const { setCredientials, logout } = authSlice.actions;
// Exporting the authSlice reducer to be used in the Redux store
export default authSlice.reducer;

@ -0,0 +1,59 @@
import { createSlice } from "@reduxjs/toolkit";
import { updateCart } from "../../../Utils/cartUtils.js";
const initialState = localStorage.getItem("cart")
? JSON.parse(localStorage.getItem("cart"))
: { cartItems: [], shippingAddress: {}, paymentMethod: "PayPal" };
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addToCart: (state, action) => {
const { user, rating, numReviews, reviews, ...item } = action.payload;
const existItem = state.cartItems.find((x) => x._id === item._id);
if (existItem) {
state.cartItems = state.cartItems.map((x) =>
x._id === existItem._id ? item : x
);
} else {
state.cartItems = [...state.cartItems, item];
}
return updateCart(state, item);
},
removeFromCart: (state, action) => {
state.cartItems = state.cartItems.filter((x) => x._id !== action.payload);
return updateCart(state);
},
saveShippingAddress: (state, action) => {
state.shippingAddress = action.payload;
localStorage.setItem("cart", JSON.stringify(state));
},
savePaymentMethod: (state, action) => {
state.paymentMethod = action.payload;
localStorage.setItem("cart", JSON.stringify(state));
},
clearCartItems: (state, action) => {
state.cartItems = [];
localStorage.setItem("cart", JSON.stringify(state));
},
resetCart: (state) => (state = initialState),
},
});
export const {
addToCart,
removeFromCart,
savePaymentMethod,
saveShippingAddress,
clearCartItems,
resetCart,
} = cartSlice.actions;
export default cartSlice.reducer;

@ -0,0 +1,28 @@
import { createSlice } from "@reduxjs/toolkit";
const favoriteSlice = createSlice({
name: 'favorites',
initialState: [],
reducers: {
addToFavorites: ( state, action ) => {
if(!state.some((product) => product._id === action.payload._id)){
state.push(action.payload)
}
},
removeFromFavorites: (state, action) => {
return state.filter((product) => product._id !== action.payload._id)
},
setFavorites: (state, action) => {
return action.payload
}
}
})
export const {
addToFavorites,
removeFromFavorites,
setFavorites,
} = favoriteSlice.actions
export const selectFavoriteProduct = (state) => state.favorites
export default favoriteSlice.reducer

@ -0,0 +1,42 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
categories: [],
products: [],
checked: [],
radio: [],
brandCheckboxes: {},
checkedBrands: [],
};
const shopSlice = createSlice({
name: "shop",
initialState,
reducers: {
setCategories: (state, action) => {
state.categories = action.payload;
},
setProducts: (state, action) => {
state.products = action.payload;
},
setChecked: (state, action) => {
state.checked = action.payload;
},
setRadio: (state, action) => {
state.radio = action.payload;
},
setSelectedBrand: (state, action) => {
state.selectedBrand = action.payload;
},
},
});
export const {
setCategories,
setProducts,
setChecked,
setRadio,
setSelectedBrand,
} = shopSlice.actions;
export default shopSlice.reducer;

@ -0,0 +1,45 @@
// Importing configureStore function from Redux Toolkit for creating a Redux store
import { configureStore } from "@reduxjs/toolkit";
// Importing setupListeners function from Redux Toolkit for setting up API query listeners
import { setupListeners } from "@reduxjs/toolkit/query/react";
// Importing the API slice created using createApi from the apiSlice file
import { apiSlice } from "./api/apiSlice";
// Importing the authReducer from the authSlice file
import authReducer from "./features/auth/authSlice";
import favoritesReducer from "../redux/features/favorites/favoriteSlice";
import CartSliceReducer from "../redux/features/cart/cartSlice"
import { getFavoritesFromLocalStorage } from "../Utils/localStorage";
import shopReducer from "../redux/features/shop/shopSlice";
const initialFavorites = getFavoritesFromLocalStorage() || [];
// Creating the Redux store with the configured reducers, middleware, and devTools settings
const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer, // Including the API slice reducer under a specific key
auth: authReducer, // Including the authReducer for handling authentication state
favorites: favoritesReducer,
cart: CartSliceReducer,
shop: shopReducer,
},
preloadedState: {
favorites: initialFavorites,
},
// Configuring middleware to include the API middleware along with the default middleware
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware),
// Enabling Redux DevTools extension for debugging
devTools: true,
});
// Setting up API query listeners with the Redux store's dispatch function
setupListeners(store.dispatch);
// Exporting the configured Redux store for use in the application
export default store;

@ -0,0 +1,13 @@
import flowbitePlugin from 'flowbite/plugin'
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [flowbitePlugin],
}

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/b6/": "http://localhost:4006",
"/uploads/": "http://localhost:4006"
}
},
})

1865
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,29 @@
{
"name": "capstone3fs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"backend": "nodemon backend/index.js",
"frontend": "npm run dev --prefix frontend",
"dev": "concurrently \"npm run frontend\" \"npm run backend\" "
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"concurrently": "^8.2.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"express-formidable": "^1.2.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.1",
"multer": "^1.4.5-lts.1",
"nodemon": "^3.0.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Loading…
Cancel
Save