capstone 3 files
@ -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
|
@ -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>
|
@ -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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 437 KiB |
After Width: | Height: | Size: 470 KiB |
After Width: | Height: | Size: 459 KiB |
After Width: | Height: | Size: 406 KiB |
After Width: | Height: | Size: 484 KiB |
After Width: | Height: | Size: 477 KiB |
After Width: | Height: | Size: 486 KiB |
After Width: | Height: | Size: 488 KiB |
After Width: | Height: | Size: 449 KiB |
After Width: | Height: | Size: 486 KiB |
After Width: | Height: | Size: 473 KiB |
After Width: | Height: | Size: 473 KiB |