Capstone-3 Update
@ -0,0 +1,4 @@
|
|||||||
|
PORT=4000
|
||||||
|
MONGO_URL=mongodb+srv://ronreciproco123:admin123@cluster0.9ao1xec.mongodb.net
|
||||||
|
SECRET_SAUCE=cornhub
|
||||||
|
BASE_URL=http://localhost:4000
|
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
// Middleware for verifying and authenticating JWT token
|
||||||
|
exports.authenticateToken = (req, res, next) => {
|
||||||
|
try {
|
||||||
|
// Extract the token from the Authorization header
|
||||||
|
const authHeader = req.header('Authorization');
|
||||||
|
|
||||||
|
if (!authHeader) {
|
||||||
|
return res.status(401).json({ message: 'Unauthorized. Token not provided.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.replace('Bearer ', '');
|
||||||
|
|
||||||
|
console.log('Token:', token); // Log the token to the console
|
||||||
|
|
||||||
|
const decoded = jwt.verify(token, process.env.SECRET_SAUCE);
|
||||||
|
req.user = decoded; // Attach the decoded information to the request for future use
|
||||||
|
|
||||||
|
// Debugging purposes
|
||||||
|
console.log('userId:', decoded.userId);
|
||||||
|
console.log('isAdmin:', decoded.isAdmin);
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('User token expired.');
|
||||||
|
|
||||||
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
|
return res.status(401).json({ message: 'Unauthorized. Token has expired.' });
|
||||||
|
} else {
|
||||||
|
return res.status(401).json({ message: 'Unauthorized. Invalid token.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Function to generate a JWT token
|
||||||
|
exports.generateToken = (userId, email, isAdmin) => {
|
||||||
|
return jwt.sign({ userId, email, isAdmin }, process.env.SECRET_SAUCE, {
|
||||||
|
expiresIn: "3h",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware to verify admin status
|
||||||
|
exports.verifyAdmin = (req, res, next) => {
|
||||||
|
if (req.user && req.user.isAdmin) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.json({ message: "Action Forbidden. User is not an admin." })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.verifyUser = (req, res, next) => {
|
||||||
|
const authenticatedUserId = req.user.userId;
|
||||||
|
const requestedUserId = req.body.userId;
|
||||||
|
|
||||||
|
if (authenticatedUserId && authenticatedUserId === requestedUserId) {
|
||||||
|
// User is authenticated, and the requested userId matches the authenticated userId
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
return res.status(403).json({
|
||||||
|
message: "Permission denied.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
exports.extractAnonymousUserId = (req, res, next) => {
|
||||||
|
req.anonymousUserId = req.headers['x-anonymous-user-id'];
|
||||||
|
next();
|
||||||
|
};
|
@ -0,0 +1,101 @@
|
|||||||
|
|
||||||
|
const Cart = require('../model/Cart');
|
||||||
|
|
||||||
|
|
||||||
|
exports.addToCart = async (req, res) => {
|
||||||
|
const { userId } = req.body;
|
||||||
|
const { productId, quantity } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let cart = await Cart.findOne({ userId }).populate('items.product');
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
cart = new Cart({
|
||||||
|
userId,
|
||||||
|
items: [{ product: productId, quantity }],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const existingItem = cart.items.find(item => item.product._id.toString() === productId);
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.quantity += quantity;
|
||||||
|
} else {
|
||||||
|
cart.items.push({ product: productId, quantity });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await cart.save();
|
||||||
|
|
||||||
|
console.log(`Product with ID ${productId} added to cart for user with ID ${userId}`);
|
||||||
|
|
||||||
|
res.json({ message: 'Item added to cart successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.updateCart = async (req, res) => {
|
||||||
|
const { userId, product, quantity } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let cart = await Cart.findOne({ userId });
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
return res.status(404).json({ error: 'Cart not found for the user' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingItem = cart.items.find(item => item.product.toString() === product);
|
||||||
|
|
||||||
|
if (!existingItem) {
|
||||||
|
return res.status(404).json({ error: 'Product not found in the cart' });
|
||||||
|
}
|
||||||
|
|
||||||
|
existingItem.quantity = quantity;
|
||||||
|
|
||||||
|
await cart.save();
|
||||||
|
|
||||||
|
res.json({ message: 'Quantity updated successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.removeFromCart = async (req, res) => {
|
||||||
|
const { userId, productId } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let cart = await Cart.findOne({ userId });
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
return res.status(404).json({ error: 'Cart not found for the user' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the product from the cart
|
||||||
|
cart.items = cart.items.filter(item => item.productId.toString() !== productId);
|
||||||
|
|
||||||
|
await cart.save();
|
||||||
|
|
||||||
|
res.json({ message: 'Product removed from cart successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assuming you have middleware that adds user information to the request object
|
||||||
|
exports.getCartDetails = async (req, res) => {
|
||||||
|
const { userId } = req.params;
|
||||||
|
try {
|
||||||
|
const cart = await Cart.findOne({ userId });
|
||||||
|
if (!cart) {
|
||||||
|
return res.status(404).json({ message: 'Cart not found for the user' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ items: cart.items });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching cart details:', error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,227 @@
|
|||||||
|
|
||||||
|
const Cart = require('../model/Cart');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
require("dotenv").config()
|
||||||
|
|
||||||
|
exports.addToCart = async (req, res) => {
|
||||||
|
let { userId, anonymousUserId } = req.body;
|
||||||
|
const { productId, quantity } = req.body;
|
||||||
|
|
||||||
|
if (!userId && !anonymousUserId) {
|
||||||
|
return res.status(400).json({ error: 'User ID or anonymous ID is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!userId && anonymousUserId) {
|
||||||
|
// If anonymousUserId is present, create a separate entry in the database
|
||||||
|
const cart = new Cart({
|
||||||
|
anonymousUserId,
|
||||||
|
items: [{ product: productId, quantity }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await cart.save();
|
||||||
|
|
||||||
|
console.log(`Product with ID ${productId} added to cart for anonymous user with ID ${anonymousUserId}`);
|
||||||
|
|
||||||
|
return res.json({ message: 'Item added to cart successfully', userId: anonymousUserId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If userId is provided, use it for the database query
|
||||||
|
let cart = await Cart.findOne({ userId }).populate('items.product');
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
cart = new Cart({
|
||||||
|
userId,
|
||||||
|
items: [{ product: productId, quantity }],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const existingItem = cart.items.find(item => item.product._id.toString() === productId);
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.quantity += quantity;
|
||||||
|
} else {
|
||||||
|
cart.items.push({ product: productId, quantity });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await cart.save();
|
||||||
|
|
||||||
|
console.log(`Product with ID ${productId} added to cart for user with ID ${userId}`);
|
||||||
|
|
||||||
|
res.json({ message: 'Item added to cart successfully', userId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
exports.updateCart = async (req, res) => {
|
||||||
|
let { userId } = req.body;
|
||||||
|
const { updates } = req.body;
|
||||||
|
|
||||||
|
// If userId is 'anonymousUserId', use a constant user ID
|
||||||
|
if (userId === 'anonymousUserId') {
|
||||||
|
userId = 'constantAnonymousUserId';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(400).json({ error: 'User ID is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let cart = await Cart.findOne({ userId });
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
return res.status(404).json({ error: 'Cart not found for the user' });
|
||||||
|
}
|
||||||
|
|
||||||
|
updates.forEach((update) => {
|
||||||
|
const { product, quantity } = update;
|
||||||
|
|
||||||
|
const existingItem = cart.items.find(
|
||||||
|
(item) => item.product.toString() === product.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingItem) {
|
||||||
|
return res.status(404).json({ error: 'Product not found in the cart' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quantity === 0) {
|
||||||
|
// If the quantity is 0, remove the product from the cart
|
||||||
|
cart.items = cart.items.filter(
|
||||||
|
(item) => item.product.toString() !== product.toString()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
existingItem.quantity = quantity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await cart.save();
|
||||||
|
|
||||||
|
res.json({ message: 'Cart updated successfully', userId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.removeFromCart = async (req, res) => {
|
||||||
|
let { userId } = req.body;
|
||||||
|
const { product } = req.body;
|
||||||
|
|
||||||
|
// If userId is 'anonymousUserId', use a constant user ID
|
||||||
|
if (userId === 'anonymousUserId') {
|
||||||
|
userId = 'constantAnonymousUserId';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(400).json({ error: 'User ID is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let cart = await Cart.findOne({ userId });
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
return res.status(404).json({ error: 'Cart not found for the user' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the product from the cart
|
||||||
|
cart.items = cart.items.filter(item => item.product.toString() !== product);
|
||||||
|
|
||||||
|
await cart.save();
|
||||||
|
|
||||||
|
res.json({ message: 'Product removed from cart successfully', userId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main getCartDetails function that handles both cases
|
||||||
|
exports.getCartDetails = async (req, res) => {
|
||||||
|
const { userId } = req.params;
|
||||||
|
try {
|
||||||
|
const cart = await Cart.findOne({ userId });
|
||||||
|
if (!cart) {
|
||||||
|
return res.status(404).json({ message: 'Cart not found for the user' });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ items: cart.items });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching cart details:', error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to get cart details by anonymous user ID
|
||||||
|
const getCartDetailsByAnonymousUserId = async (userId) => {
|
||||||
|
try {
|
||||||
|
const cart = await Cart.findByAnonymousUserId(userId);
|
||||||
|
|
||||||
|
console.log('Cart found:', cart);
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
return { status: 404, message: "Cart not found for the user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return cart;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// New function for anonymous user cart details
|
||||||
|
exports.getCartDetailsForAnonymousUser = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const anonymousUserId = req.params.anonymousUserId;
|
||||||
|
|
||||||
|
console.log("Received anonymousUserId:", anonymousUserId);
|
||||||
|
|
||||||
|
if (anonymousUserId) {
|
||||||
|
const cartDetails = await getCartDetailsByAnonymousUserId(anonymousUserId);
|
||||||
|
if (!cartDetails) {
|
||||||
|
return res.status(404).json({ message: "Cart not found for the user" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ items: cartDetails.items });
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ error: "Invalid request" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching cart details for anonymous user:", error);
|
||||||
|
res.status(500).json({ error: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.clearCart = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const token = req.headers.authorization.split(' ')[1];
|
||||||
|
const anonymousUserId = req.headers['x-anonymous-user-id'];
|
||||||
|
|
||||||
|
// Check if the request is from an authenticated user
|
||||||
|
if (token) {
|
||||||
|
|
||||||
|
const decoded = jwt.verify(token, process.env.SECRET_SAUCE) // Replace 'your-secret-key' with your actual secret key
|
||||||
|
const userId = decoded.userId;
|
||||||
|
|
||||||
|
// Clear the cart for the authenticated user
|
||||||
|
await Cart.findOneAndDelete({ userId });
|
||||||
|
} else if (anonymousUserId) {
|
||||||
|
// Clear the cart for the anonymous user
|
||||||
|
await Cart.findOneAndDelete({ anonymousUserId });
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: 'Invalid request. Missing user information.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ message: 'Cart cleared successfully.' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing cart:', error);
|
||||||
|
return res.status(500).json({ error: 'Internal server error.' });
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
const User = require("../model/User")
|
||||||
|
|
||||||
|
|
||||||
|
exports.createOrder = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId, products, totalAmount } = req.body
|
||||||
|
|
||||||
|
const user = await User.findById(userId)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: "User not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user is an admin
|
||||||
|
if (user.isAdmin) {
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.json({ message: "Admins cannot create orders" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const newOrder = {
|
||||||
|
products: products,
|
||||||
|
totalAmount: totalAmount,
|
||||||
|
purchaseOn: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
user.orderedProducts.push(newOrder)
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: "Order created successfully",
|
||||||
|
order: newOrder,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).json({ message: "Internal Server Error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve authenticated user's orders
|
||||||
|
exports.getOrders = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.body
|
||||||
|
|
||||||
|
const user = await User.findById(userId)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: "User not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the user details in the response
|
||||||
|
res.send({ orderedProducts: user.orderedProducts })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).json({ message: "Internal server error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exports.getAllOrders = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const orders = await User.find({ "orderedProducts.0": { $exists: true } }, "orderedProducts");
|
||||||
|
res.status(200).json({ success: true, data: orders });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,232 @@
|
|||||||
|
const Product = require("../model/Product")
|
||||||
|
const fs = require('fs');
|
||||||
|
require("dotenv").config();
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
|
||||||
|
exports.editProduct = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const productId = req.params.productId;
|
||||||
|
const updatedProductData = req.body;
|
||||||
|
|
||||||
|
// Validate or sanitize the input data if needed
|
||||||
|
|
||||||
|
// Find the product by ID and update it
|
||||||
|
const updatedProduct = await Product.findByIdAndUpdate(
|
||||||
|
productId,
|
||||||
|
updatedProductData,
|
||||||
|
{ new: true } // This option ensures you get the updated product in the response
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updatedProduct) {
|
||||||
|
return res.status(404).json({ message: 'Product not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the updated product in the response
|
||||||
|
res.status(200).json(updatedProduct);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error editing product:', error);
|
||||||
|
res.status(500).json({ message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.productTag = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { tag } = req.query;
|
||||||
|
|
||||||
|
// If no specific tag is provided, return all products
|
||||||
|
const filter = tag ? { tags: tag } : {};
|
||||||
|
|
||||||
|
// Fetch products from the database based on the filter
|
||||||
|
const products = await Product.find(filter);
|
||||||
|
|
||||||
|
res.json(products); // Ensure this line sends a JSON array, even if empty ([] for no products).
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching tagged products:', error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createProduct = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, description, price, isActive } = req.body;
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
const imagePath = req.file.path;
|
||||||
|
|
||||||
|
const newProduct = new Product({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
price,
|
||||||
|
isActive,
|
||||||
|
image: `http://localhost:4000/uploads/${path.basename(imagePath)}`, // Store the image link
|
||||||
|
});
|
||||||
|
|
||||||
|
await newProduct.save();
|
||||||
|
|
||||||
|
return res.status(201).json(newProduct);
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: 'Image is required' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.deleteProduct = async (req, res) => {
|
||||||
|
const productId = req.params.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the product by ID
|
||||||
|
const product = await Product.findById(productId);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return res.status(404).json({ message: 'Product not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the associated image file from the "uploads" folder
|
||||||
|
if (product.image) {
|
||||||
|
const imagePath = path.join(__dirname, '../uploads', path.basename(product.image));
|
||||||
|
fs.unlinkSync(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use deleteOne method to delete the product
|
||||||
|
await product.deleteOne();
|
||||||
|
|
||||||
|
res.json({ message: 'Product deleted successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Controller function for retrieving all products (accessible to both admin and normal user)
|
||||||
|
exports.getAllProducts = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const products = await Product.find()
|
||||||
|
|
||||||
|
res.status(200).json(products)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).json({ message: "Internal server error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller function for retrieving all active products (accessible to both admin and normal user)
|
||||||
|
exports.getActiveProducts = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const activeProducts = await Product.find({ isActive: true })
|
||||||
|
|
||||||
|
res.status(200).json(activeProducts)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).json({ message: "Internal server error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller to retrieve a single product by id
|
||||||
|
exports.getProductById = async (req, res) => {
|
||||||
|
const productId = req.params.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const product = await Product.findById(productId);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return res.status(404).json({ message: 'Product not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(product);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching product by ID:', error);
|
||||||
|
res.status(500).json({ message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Controller function to update product information
|
||||||
|
exports.updateProduct = async (req, res) => {
|
||||||
|
const productId = req.params.id;
|
||||||
|
const { name, description, price, isActive } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if the product exists
|
||||||
|
const existingProduct = await Product.findById(productId);
|
||||||
|
if (!existingProduct) {
|
||||||
|
return res.status(404).json({ message: 'Product not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update product information
|
||||||
|
existingProduct.name = name;
|
||||||
|
existingProduct.description = description;
|
||||||
|
existingProduct.price = price;
|
||||||
|
existingProduct.isActive = isActive;
|
||||||
|
|
||||||
|
// Save the updated product
|
||||||
|
const updatedProduct = await existingProduct.save();
|
||||||
|
|
||||||
|
res.status(200).json(updatedProduct);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.activateProduct = async (req, res) => {
|
||||||
|
const { productId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the product by ID
|
||||||
|
const product = await Product.findById(productId);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return res.status(404).json({ message: 'Product not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the product is already active
|
||||||
|
if (product.isActive) {
|
||||||
|
return res.status(400).json({ message: 'Product is already active' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate the product
|
||||||
|
product.isActive = true;
|
||||||
|
|
||||||
|
// Save the updated product
|
||||||
|
await product.save();
|
||||||
|
|
||||||
|
res.json({ message: 'Product activated successfully', product });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.archiveProduct = async (req, res) => {
|
||||||
|
const { productId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the product by ID
|
||||||
|
const product = await Product.findById(productId);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return res.status(404).json({ message: 'Product not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the product is already active
|
||||||
|
if (!product.isActive) {
|
||||||
|
return res.status(400).json({ message: 'Product is already in Archive' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate the product
|
||||||
|
product.isActive = false;
|
||||||
|
|
||||||
|
// Save the updated product
|
||||||
|
await product.save();
|
||||||
|
|
||||||
|
res.json({ message: 'Product successfuly archived', product });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,535 @@
|
|||||||
|
const jwt = require("jsonwebtoken")
|
||||||
|
const bcrypt = require("bcrypt")
|
||||||
|
const faker = require("faker")
|
||||||
|
const User = require("../model/User")
|
||||||
|
const Cart = require('../model/Cart');
|
||||||
|
const Address = require('../model/Address');
|
||||||
|
const PromoCode = require("../model/PromoCode")
|
||||||
|
const auth = require("../auth")
|
||||||
|
const { validationResult } = require("express-validator")
|
||||||
|
|
||||||
|
// Secret Sauce
|
||||||
|
require("dotenv").config()
|
||||||
|
|
||||||
|
exports.deletePromoCode = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const promoCodeId = req.params.id
|
||||||
|
|
||||||
|
// Use Mongoose to find and remove the promo code
|
||||||
|
const deletedPromoCode = await PromoCode.findOneAndDelete({
|
||||||
|
_id: promoCodeId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!deletedPromoCode) {
|
||||||
|
return res.status(404).json({ message: "Promo code not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ message: "Promo code deleted successfully" })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting promo code:", error)
|
||||||
|
res.status(500).json({
|
||||||
|
error: "An error occurred while deleting promo code",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.showPromoCodes = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Fetch all promo codes from the database
|
||||||
|
const promoCodes = await PromoCode.find()
|
||||||
|
|
||||||
|
res.status(200).json({ promoCodes })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching promo codes:", error)
|
||||||
|
res.status(500).json({
|
||||||
|
error: "An error occurred while fetching promo codes",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createPromoCode = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { code, discountAmount, expirationDate } = req.body
|
||||||
|
|
||||||
|
// Check if the promo code already exists
|
||||||
|
const existingPromoCode = await PromoCode.findOne({ code })
|
||||||
|
|
||||||
|
if (existingPromoCode) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ message: "Promo code already exists" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new promo code
|
||||||
|
const newPromoCode = new PromoCode({
|
||||||
|
code,
|
||||||
|
discountAmount,
|
||||||
|
expirationDate,
|
||||||
|
// Add more fields as needed
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save the promo code to the database
|
||||||
|
await newPromoCode.save()
|
||||||
|
|
||||||
|
res.status(201).json({ message: "Promo code created successfully" })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating promo code:", error)
|
||||||
|
res.status(500).json({ message: "Internal server error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.applyPromoCode = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Validate request using express-validator
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve promo code from request body
|
||||||
|
const { promoCode } = req.body;
|
||||||
|
|
||||||
|
// Check if promo code exists in the database
|
||||||
|
const existingPromoCode = await PromoCode.findOne({ code: promoCode });
|
||||||
|
|
||||||
|
if (!existingPromoCode) {
|
||||||
|
// If promo code does not exist, return an error response
|
||||||
|
return res.status(400).json({ message: "Invalid promo code" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the discount amount
|
||||||
|
console.log("Discount Amount:", existingPromoCode.discountAmount);
|
||||||
|
|
||||||
|
// Additional logic to calculate discount and update total price
|
||||||
|
// Assuming totalPrice is provided in the request or can be fetched from somewhere
|
||||||
|
const totalPrice = 100; // Replace this with actual total price
|
||||||
|
|
||||||
|
// Calculate discounted amount based on the discountAmount
|
||||||
|
const discountAmount = existingPromoCode.discountAmount;
|
||||||
|
|
||||||
|
// Subtract discounted amount from the current total price
|
||||||
|
const updatedTotalPrice = totalPrice - discountAmount;
|
||||||
|
|
||||||
|
// Log the updated total price
|
||||||
|
console.log("Updated Total Price:", updatedTotalPrice);
|
||||||
|
|
||||||
|
// Return success response
|
||||||
|
return res.status(200).json({
|
||||||
|
message: "Promo code applied successfully",
|
||||||
|
promoCode: existingPromoCode.code,
|
||||||
|
discountAmount,
|
||||||
|
updatedTotalPrice,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error applying promo code:", error);
|
||||||
|
return res.status(500).json({ message: "Internal server error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.resetPassword = async (req, res) => {
|
||||||
|
const { email, newPassword, resetToken } = req.body
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if the reset token is valid (you may store reset tokens in your database)
|
||||||
|
const user = await User.findOne({ email, resetToken })
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ message: "Invalid reset token or email." })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the reset token is still valid (e.g., not expired)
|
||||||
|
|
||||||
|
// Reset the password
|
||||||
|
user.password = newPassword
|
||||||
|
user.resetToken = null // Invalidate the reset token after use
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Password reset successful." })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error resetting password:", error)
|
||||||
|
return res.status(500).json({ message: "Internal server error." })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.userDetails = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Fetch user details from the database using the authenticated user ID
|
||||||
|
const user = await User.findById(req.user.userId)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: "User not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the token is expired
|
||||||
|
const token = req.header("Authorization").replace("Bearer ", "")
|
||||||
|
const decoded = jwt.verify(token, process.env.SECRET_SAUCE)
|
||||||
|
|
||||||
|
if (decoded.exp < Date.now() / 1000) {
|
||||||
|
// Token is expired
|
||||||
|
return res
|
||||||
|
.status(401)
|
||||||
|
.json({ message: "Unauthorized. Token has expired." })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the user details in the response
|
||||||
|
res.json({
|
||||||
|
_id: user._id,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
mobileNo: user.mobileNo,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
orderedProducts: user.orderedProducts,
|
||||||
|
// Add other user details you want to include
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching user details:", error)
|
||||||
|
|
||||||
|
if (error.name === "TokenExpiredError") {
|
||||||
|
return res
|
||||||
|
.status(401)
|
||||||
|
.json({ message: "Unauthorized. Token has expired." })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({ message: "Internal server error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.checkEmail = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email } = req.body
|
||||||
|
|
||||||
|
const existingUser = await User.findOne({ email })
|
||||||
|
|
||||||
|
res.json({ exists: !!existingUser })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking email:", error)
|
||||||
|
res.status(500).json({ error: "Internal Server Error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller function for user registration
|
||||||
|
exports.registerUser = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, password, firstName, lastName } = req.body
|
||||||
|
|
||||||
|
// If firstName and lastName are not provided, generate default values using faker
|
||||||
|
const autoGeneratedFirstName = firstName || faker.name.firstName()
|
||||||
|
const autoGeneratedLastName = lastName || faker.name.lastName()
|
||||||
|
|
||||||
|
// Hash the password before saving it
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10)
|
||||||
|
|
||||||
|
// Set mobileNo to null by default
|
||||||
|
const newUser = new User({
|
||||||
|
email,
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: autoGeneratedFirstName,
|
||||||
|
lastName: autoGeneratedLastName,
|
||||||
|
mobileNo: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
await newUser.save()
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message:
|
||||||
|
"User registered successfully. To update account details, access user/update",
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).json({ message: "Internal server error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller function for user authentication
|
||||||
|
exports.authenticateUser = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
const user = await User.findOne({ email });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({ message: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordMatch = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if (!passwordMatch) {
|
||||||
|
return res.status(401).json({ message: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are existing cart items with anonymousUserId
|
||||||
|
const anonymousUserId = req.headers['x-anonymous-user-id'];
|
||||||
|
const existingCart = await Cart.findOne({ userId: anonymousUserId }).populate('items.product');
|
||||||
|
|
||||||
|
if (existingCart) {
|
||||||
|
// Update the userId for existing cart items
|
||||||
|
existingCart.userId = user._id; // Assuming you have the user object
|
||||||
|
await existingCart.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token using the function from auth.js
|
||||||
|
const token = auth.generateToken(user._id, user.email, user.isAdmin);
|
||||||
|
|
||||||
|
// Decode JWT token to get expiration time
|
||||||
|
const decodedToken = jwt.decode(token);
|
||||||
|
|
||||||
|
if (decodedToken) {
|
||||||
|
const expiration = new Date(decodedToken.exp * 1000); // Convert seconds to milliseconds
|
||||||
|
// Log token expiration
|
||||||
|
console.log(`Authenticate success. Token will expire on: ${expiration}`);
|
||||||
|
} else {
|
||||||
|
console.error('Error decoding token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return user details and token
|
||||||
|
res.status(200).json({
|
||||||
|
userId: user._id,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
mobileNo: user.mobileNo,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
token: token,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.updateUserData = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
userId,
|
||||||
|
newEmail,
|
||||||
|
newFirstName,
|
||||||
|
newLastName,
|
||||||
|
newPassword,
|
||||||
|
newMobileNo,
|
||||||
|
} = req.body
|
||||||
|
|
||||||
|
const userIdFromToken = req.user.userId
|
||||||
|
|
||||||
|
if (userIdFromToken !== userId) {
|
||||||
|
return res.status(403).json({
|
||||||
|
message:
|
||||||
|
"Permission denied. You can only update your own data.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findById(userId)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: "User not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update email if provided
|
||||||
|
if (newEmail) {
|
||||||
|
user.email = newEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update firstName if provided
|
||||||
|
if (newFirstName) {
|
||||||
|
user.firstName = newFirstName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update lastName if provided
|
||||||
|
if (newLastName) {
|
||||||
|
user.lastName = newLastName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password if provided
|
||||||
|
if (newPassword) {
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 10)
|
||||||
|
user.password = hashedPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update mobileNo if provided
|
||||||
|
if (newMobileNo) {
|
||||||
|
user.mobileNo = newMobileNo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated user data
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
// Fetch the updated user details
|
||||||
|
const updatedUser = await User.findById(userId)
|
||||||
|
|
||||||
|
// Return the complete updated user details in the response
|
||||||
|
res.status(200).json({
|
||||||
|
message: "User data updated successfully",
|
||||||
|
user: {
|
||||||
|
_id: updatedUser._id,
|
||||||
|
email: updatedUser.email,
|
||||||
|
isAdmin: updatedUser.isAdmin,
|
||||||
|
firstName: updatedUser.firstName,
|
||||||
|
lastName: updatedUser.lastName,
|
||||||
|
mobileNo: updatedUser.mobileNo,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).json({ message: "Internal server error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getUserDetails = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.body
|
||||||
|
|
||||||
|
const user = await User.findById(userId)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: "User not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new object with only the desired properties (excluding password)
|
||||||
|
const userWithoutPassword = {
|
||||||
|
_id: user._id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
orderedProducts: user.orderedProducts,
|
||||||
|
// Add other properties you want to include in the response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the user details in the response
|
||||||
|
res.status(200).json({
|
||||||
|
user: userWithoutPassword,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).json({ message: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.setAdmin = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.body // assuming userId is sent in the request body
|
||||||
|
|
||||||
|
const user = await User.findById(userId)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: "User not found." })
|
||||||
|
}
|
||||||
|
|
||||||
|
user.isAdmin = true
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
res.json({ message: "User is now an admin." })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).json({ error: "Internal server error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exports.getAddresses = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.body.userId; // Assuming you include userId in the URL parameters
|
||||||
|
|
||||||
|
// Validate the presence of the user ID
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(400).json({ error: "User ID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch addresses for the specified user ID
|
||||||
|
const addresses = await Address.find({ userId });
|
||||||
|
|
||||||
|
res.status(200).json(addresses);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching addresses:", error);
|
||||||
|
res.status(500).json({ error: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
exports.createAddress = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { street, zipCode, city, state, country, userId } = req.body;
|
||||||
|
|
||||||
|
// Validate the presence of required fields
|
||||||
|
if (!street || !zipCode || !city || !state || !country || !userId) {
|
||||||
|
return res.status(400).json({ error: "Please fill in all required address fields" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the address
|
||||||
|
const address = new Address({
|
||||||
|
street,
|
||||||
|
zipCode,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
country,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save the address to the database
|
||||||
|
const savedAddress = await address.save();
|
||||||
|
|
||||||
|
res.status(201).json(savedAddress);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating address:", error);
|
||||||
|
res.status(500).json({ error: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.deleteAddress = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id: addressId } = req.params;
|
||||||
|
|
||||||
|
console.log('Received Address ID:', addressId);
|
||||||
|
|
||||||
|
// Check if the address with the given ID exists
|
||||||
|
const existingAddress = await Address.findById(addressId);
|
||||||
|
|
||||||
|
console.log('Existing Address:', existingAddress);
|
||||||
|
|
||||||
|
if (!existingAddress) {
|
||||||
|
return res.status(404).json({ message: 'Address not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the deletion of the address
|
||||||
|
await Address.findByIdAndDelete(addressId);
|
||||||
|
|
||||||
|
res.status(200).json({ message: 'Address deleted successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting address:', error);
|
||||||
|
res.status(500).json({ message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// In userController.js or your relevant controller file
|
||||||
|
|
||||||
|
exports.updateAddress = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id: addressId } = req.params;
|
||||||
|
const { street, zipCode, city, state, country } = req.body;
|
||||||
|
|
||||||
|
// Check if the address with the given ID exists
|
||||||
|
const existingAddress = await Address.findById(addressId);
|
||||||
|
|
||||||
|
if (!existingAddress) {
|
||||||
|
return res.status(404).json({ message: 'Address not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the address fields
|
||||||
|
existingAddress.street = street;
|
||||||
|
existingAddress.zipCode = zipCode;
|
||||||
|
existingAddress.city = city;
|
||||||
|
existingAddress.state = state;
|
||||||
|
existingAddress.country = country;
|
||||||
|
|
||||||
|
// Save the updated address
|
||||||
|
await existingAddress.save();
|
||||||
|
|
||||||
|
res.status(200).json({ message: 'Address updated successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating address:', error);
|
||||||
|
res.status(500).json({ message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,48 @@
|
|||||||
|
// Dependencies
|
||||||
|
const express = require("express");
|
||||||
|
const mongoose = require("mongoose");
|
||||||
|
const cors = require("cors");
|
||||||
|
require("dotenv").config();
|
||||||
|
const userRoute = require("./routes/user");
|
||||||
|
const productRoute = require("./routes/product");
|
||||||
|
const cartRoute = require("./routes/cart");
|
||||||
|
const orderRoute = require("./routes/order");
|
||||||
|
const path = require('path'); // Add this line
|
||||||
|
|
||||||
|
// Server start
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Middlewares
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
// Serve static files from the "uploads" folder
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||||
|
|
||||||
|
// Database
|
||||||
|
mongoose
|
||||||
|
.connect(process.env.MONGO_URL, {
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
dbName: "CAPSTONE-3",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log(`Connected to Database ${process.env.MONGO_URL}`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use("/order", orderRoute);
|
||||||
|
app.use("/user", userRoute);
|
||||||
|
app.use("/product", productRoute);
|
||||||
|
app.use("/cart", cartRoute);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Server up
|
||||||
|
app.listen(process.env.PORT || 4000, () => {
|
||||||
|
console.log(`Server is running on port ${process.env.PORT}..`);
|
||||||
|
});
|
@ -0,0 +1,14 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const addressSchema = new mongoose.Schema({
|
||||||
|
street: { type: String, required: true },
|
||||||
|
zipCode: { type: String, required: true },
|
||||||
|
city: { type: String, required: true },
|
||||||
|
state: { type: String, required: true },
|
||||||
|
country: { type: String, required: true },
|
||||||
|
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const Address = mongoose.model('Address', addressSchema);
|
||||||
|
|
||||||
|
module.exports = Address;
|
@ -0,0 +1,40 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const cartItemSchema = new mongoose.Schema({
|
||||||
|
product: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Product',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cartSchema = new mongoose.Schema({
|
||||||
|
userId: { type: String }, // For authenticated users
|
||||||
|
anonymousUserId: { type: String }, // For anonymous users
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
|
||||||
|
quantity: { type: Number, default: 1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Method to find a cart by user ID
|
||||||
|
cartSchema.statics.findByUserId = function (userId) {
|
||||||
|
return this.findOne({ userId });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Method to find a cart by anonymous user ID
|
||||||
|
cartSchema.statics.findByAnonymousUserId = function (anonymousUserId) {
|
||||||
|
return this.findOne({ anonymousUserId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const Cart = mongoose.model('Cart', cartSchema);
|
||||||
|
|
||||||
|
module.exports = Cart;
|
||||||
|
|
@ -0,0 +1,15 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const productSchema = new mongoose.Schema({
|
||||||
|
name: { type: String, required: true },
|
||||||
|
description: { type: String, required: true },
|
||||||
|
price: { type: Number, required: true },
|
||||||
|
isActive: { type: Boolean, default: false },
|
||||||
|
image: { type: String },
|
||||||
|
tags: { type: [String], default: ["default"] }, // Array of tags
|
||||||
|
createdOn: { type: Date, default: Date.now },
|
||||||
|
});
|
||||||
|
|
||||||
|
const Product = mongoose.model('Product', productSchema);
|
||||||
|
|
||||||
|
module.exports = Product;
|
@ -0,0 +1,22 @@
|
|||||||
|
// models/PromoCode.js
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const promoCodeSchema = new mongoose.Schema({
|
||||||
|
code: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
discountAmount: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
expirationDate: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
// Add more fields as needed
|
||||||
|
});
|
||||||
|
|
||||||
|
const PromoCode = mongoose.model('PromoCode', promoCodeSchema);
|
||||||
|
|
||||||
|
module.exports = PromoCode;
|
@ -0,0 +1,32 @@
|
|||||||
|
// Your user model definition
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const productSchema = new mongoose.Schema({
|
||||||
|
productId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Product',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
productName: { type: String, required: true },
|
||||||
|
quantity: { type: Number, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const userSchema = new mongoose.Schema({
|
||||||
|
email: { type: String, required: true, unique: true },
|
||||||
|
firstName: { type: String },
|
||||||
|
lastName: { type: String },
|
||||||
|
password: { type: String, required: true },
|
||||||
|
isAdmin: { type: Boolean, default: false },
|
||||||
|
mobileNo: { type: String },
|
||||||
|
orderedProducts: [
|
||||||
|
{
|
||||||
|
products: [productSchema],
|
||||||
|
totalAmount: { type: Number, required: true },
|
||||||
|
purchaseOn: { type: Date, default: Date.now },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const User = mongoose.model('User', userSchema);
|
||||||
|
|
||||||
|
module.exports = User;
|
@ -0,0 +1,11 @@
|
|||||||
|
// models/productTag.js
|
||||||
|
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const productTagSchema = new mongoose.Schema({
|
||||||
|
name: { type: String, required: true, unique: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProductTag = mongoose.model('ProductTag', productTagSchema);
|
||||||
|
|
||||||
|
module.exports = ProductTag;
|
@ -0,0 +1,22 @@
|
|||||||
|
// multer.js
|
||||||
|
|
||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Set up storage using disk storage
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, 'uploads/'); // Specify the directory where you want to store uploaded files
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
// Generate a unique filename
|
||||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||||
|
const fileExtension = path.extname(file.originalname);
|
||||||
|
cb(null, file.fieldname + '-' + uniqueSuffix + fileExtension);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a Multer instance with the storage configuration
|
||||||
|
const upload = multer({ storage: storage });
|
||||||
|
|
||||||
|
module.exports = upload;
|
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "capstone-2",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "nodemon index.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-validator": "^7.0.1",
|
||||||
|
"faker": "^5.5.3",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mongoose": "^8.0.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
|
"react-responsive": "^9.0.2",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,176 @@
|
|||||||
|
## E-COMMERCE API DOCUMENTATION
|
||||||
|
|
||||||
|
**_INSTALLATION COMMAND:_**
|
||||||
|
|
||||||
|
`npm install bcrypt cors dotenv express faker jsonwebtoken mongoose nodemon`
|
||||||
|
|
||||||
|
**_Start_**
|
||||||
|
npm start
|
||||||
|
|
||||||
|
**_TEST ACCOUNTS:_**
|
||||||
|
|
||||||
|
- Regular User:
|
||||||
|
- email: user@email.com
|
||||||
|
- pwd: user
|
||||||
|
- Admin User:
|
||||||
|
- email: admin@email.com
|
||||||
|
- pwd: admin
|
||||||
|
|
||||||
|
**_ROUTES:_**
|
||||||
|
|
||||||
|
- User registration (POST)
|
||||||
|
- http://localhost:3000/user/register
|
||||||
|
- auth header required: NO
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"email": "admin@email.com",
|
||||||
|
"password": "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
- User authentication (POST)
|
||||||
|
- http://localhost:3000/user/login
|
||||||
|
- auth header required: NO
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"email": "admin@email.com",
|
||||||
|
"password": "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
- Create Product (Admin only) (POST)
|
||||||
|
- http://localhost:4000/product/create
|
||||||
|
- auth header required: YES
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"name": "Poring Card",
|
||||||
|
"description": "Description unknown",
|
||||||
|
"price": 500
|
||||||
|
}
|
||||||
|
|
||||||
|
- Update Profile
|
||||||
|
- http://localhost:3000/user/update
|
||||||
|
- auth header required: YES
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"userId": "",
|
||||||
|
"newEmail": "",
|
||||||
|
"newFirstname": "",
|
||||||
|
"newLastName": "",
|
||||||
|
"newPassword": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
- Retrieve all products (Admin only) (GET)
|
||||||
|
- http://localhost:3000/product/all
|
||||||
|
- auth header required: YES
|
||||||
|
- request body: none
|
||||||
|
|
||||||
|
- Retrieve all active products (GET)
|
||||||
|
- http://localhost:3000/product/active
|
||||||
|
- auth header required: NO
|
||||||
|
- request body: none
|
||||||
|
|
||||||
|
- Get all products (GET)
|
||||||
|
- http://localhost:3000/product/active
|
||||||
|
- auth header required: NO
|
||||||
|
- request body: none
|
||||||
|
|
||||||
|
- Get a product (GET)
|
||||||
|
- http://localhost:3000/product/products/65545a1e6fa9d841e1518d1d
|
||||||
|
- auth header required: YES
|
||||||
|
- request body: none
|
||||||
|
|
||||||
|
- Update Single product (PUT)
|
||||||
|
- http://localhost:3000/product/products/65545a1e6fa9d841e1518d1d
|
||||||
|
- auth header required: YES
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"name": "Christmas Cookie Card",
|
||||||
|
"description": "Updated Product Description",
|
||||||
|
"price": 29.99,
|
||||||
|
"isActive": false
|
||||||
|
}
|
||||||
|
|
||||||
|
- Create Order (POST)
|
||||||
|
- http://localhost:3000/user/order
|
||||||
|
- auth header required: YES
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"userId": "65535cb526b586a3e2fd56cc", // Replace with a valid user ID from your database
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"productId": "6553a4e897ac8ac9462f96c4", // Replace with a valid product ID from your database
|
||||||
|
"productName": "Mastering Card",
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"totalAmount": 500
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- Activate / Archive Product (PUT)
|
||||||
|
- auth header required: YES
|
||||||
|
- request body: none
|
||||||
|
- http://localhost:3000/product/products/6554634e5cac4bcd6f2394ed/activate
|
||||||
|
- http://localhost:3000/product/products/6554634e5cac4bcd6f2394ed/archive
|
||||||
|
|
||||||
|
- Set User to Admin (POST) [Admin Only]
|
||||||
|
- hhttp://localhost:3000/user/set-admin/
|
||||||
|
- auth header required: YES
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"userId":
|
||||||
|
}
|
||||||
|
|
||||||
|
- Retrieve All Orders [Admin Only] (GET)
|
||||||
|
- http://localhost:3000/user/orders-all
|
||||||
|
- auth header required: YES
|
||||||
|
- request body: none
|
||||||
|
|
||||||
|
- Add To Cart (POST)
|
||||||
|
- http://localhost:3000/cart/add-to-cart
|
||||||
|
- auth header required: YES
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"userId": "655396dcc8ea29f42422e214",
|
||||||
|
"productId": "6553a54566c4c86c39034b55",
|
||||||
|
"quantity": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
- Delete Item (DELETE)
|
||||||
|
- http://localhost:3000/cart/remove-from-cart
|
||||||
|
- auth header required: YES
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"userId": "655396dcc8ea29f42422e214",
|
||||||
|
"productId": "6553a54566c4c86c39034b55",
|
||||||
|
"quantity": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
- Update Quantity (PUT)
|
||||||
|
- http://localhost:3000/cart//update-quantity
|
||||||
|
- auth header required: YES
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"userId": "655396dcc8ea29f42422e214",
|
||||||
|
"productId": "6553a55666c4c86c39034b59",
|
||||||
|
"quantity": 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
- Cart Total (GET)
|
||||||
|
- http://localhost:3000/cart/cart-details
|
||||||
|
- auth header required: YES
|
||||||
|
- request body:
|
||||||
|
{
|
||||||
|
"userId": "655396dcc8ea29f42422e214"
|
||||||
|
}
|
||||||
|
|
||||||
|
\***\* Stretch Goals \*\***
|
||||||
|
|
||||||
|
- Set user as Admin ( Admin Only )
|
||||||
|
- Retrieve Authenticated User's Orders
|
||||||
|
- Retrieve all orders ( Admin Only )
|
||||||
|
- Add to Cart ( Added Products, Change Product Quantities, Remove Products From Cart, Subtotal for each item, Total price for all items)
|
||||||
|
- Authentication Token with expiration (1hr)
|
||||||
|
- dotenv
|
||||||
|
- faker (Auto Generate Names)
|
||||||
|
- getUserDetails function ( Detects if the user tries to get the details of the other useId's + Token auth)
|
||||||
|
- Middleware Secure verification that match Token and UserId to next()
|
@ -0,0 +1,24 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const cartController = require('../controllers/cart');
|
||||||
|
const auth = require("../auth");
|
||||||
|
|
||||||
|
const { authenticateToken, extractAnonymousUserId } = auth;
|
||||||
|
|
||||||
|
// Route to add an item to the cart
|
||||||
|
router.post('/add-to-cart', extractAnonymousUserId, cartController.addToCart);
|
||||||
|
|
||||||
|
// Update cart route
|
||||||
|
router.put('/update-cart', extractAnonymousUserId, cartController.updateCart);
|
||||||
|
|
||||||
|
router.delete('/remove-from-cart', extractAnonymousUserId, cartController.removeFromCart);
|
||||||
|
|
||||||
|
// Route to get cart details for a specific user
|
||||||
|
router.get('/user/:userId', cartController.getCartDetails);
|
||||||
|
|
||||||
|
// Route to get cart details for an anonymous user
|
||||||
|
router.get('/anonymous/:anonymousUserId', cartController.getCartDetailsForAnonymousUser);
|
||||||
|
|
||||||
|
router.post('/clear-cart', authenticateToken, cartController.clearCart);
|
||||||
|
|
||||||
|
module.exports = router;
|
@ -0,0 +1,19 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const orderController = require("../controllers/order")
|
||||||
|
|
||||||
|
const auth = require("../auth");
|
||||||
|
|
||||||
|
const { authenticateToken, verifyUser, verifyAdmin } = auth;
|
||||||
|
|
||||||
|
// POST /users/order
|
||||||
|
router.post("/order", authenticateToken, orderController.createOrder);
|
||||||
|
|
||||||
|
|
||||||
|
// Route to retrieve authenticated user's orders
|
||||||
|
router.post("/get-order", authenticateToken, orderController.getOrders);
|
||||||
|
|
||||||
|
// Route to retrieve all orders
|
||||||
|
router.get("/orders-all", authenticateToken, verifyAdmin, orderController.getAllOrders);
|
||||||
|
|
||||||
|
module.exports = router;
|
@ -0,0 +1,46 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const productController = require("../controllers/product")
|
||||||
|
const auth = require("../auth")
|
||||||
|
const upload = require('../multer');
|
||||||
|
|
||||||
|
const { authenticateToken, verifyAdmin, } = auth
|
||||||
|
|
||||||
|
|
||||||
|
router.put('/edit/:productId', productController.editProduct);
|
||||||
|
|
||||||
|
router.get("/product-tag", productController.productTag)
|
||||||
|
|
||||||
|
// S50
|
||||||
|
// Create a product route (accessible only by isAdmin)
|
||||||
|
// Retrieve all products route (accessible to both admin and normal user)
|
||||||
|
|
||||||
|
router.post("/create", authenticateToken, verifyAdmin, upload.single('image'), productController.createProduct)
|
||||||
|
|
||||||
|
|
||||||
|
// Retrieve all products route (accessible to both admin and normal user)
|
||||||
|
router.get("/", authenticateToken, productController.getAllProducts)
|
||||||
|
|
||||||
|
// Retrieve all products route (accessible to both admin and normal user)
|
||||||
|
router.get("/all", authenticateToken, verifyAdmin, productController.getAllProducts)
|
||||||
|
|
||||||
|
router.delete('/delete/:id', authenticateToken, verifyAdmin, productController.deleteProduct)
|
||||||
|
|
||||||
|
// Retrieve all active products route (accessible to both admin and normal user)
|
||||||
|
router.get("/active", productController.getActiveProducts)
|
||||||
|
|
||||||
|
// S51
|
||||||
|
// Retrieve a single product by ID
|
||||||
|
router.get('/:id', productController.getProductById)
|
||||||
|
|
||||||
|
// Update product route + admin verification
|
||||||
|
router.put('/:id', authenticateToken, verifyAdmin, productController.updateProduct)
|
||||||
|
|
||||||
|
// Archive a product
|
||||||
|
router.put('/:productId/archive', authenticateToken, verifyAdmin, productController.archiveProduct)
|
||||||
|
|
||||||
|
// Activate a product
|
||||||
|
router.put('/:productId/activate', authenticateToken, verifyAdmin, productController.activateProduct)
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router
|
@ -0,0 +1,46 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const userController = require("../controllers/user")
|
||||||
|
|
||||||
|
const auth = require("../auth");
|
||||||
|
|
||||||
|
const { authenticateToken, verifyAdmin, verifyUser } = auth;
|
||||||
|
|
||||||
|
router.post("/apply-promo-code", authenticateToken, userController.applyPromoCode)
|
||||||
|
|
||||||
|
router.get("/details", authenticateToken, userController.userDetails)
|
||||||
|
|
||||||
|
// User registration route
|
||||||
|
router.post("/register", userController.registerUser)
|
||||||
|
|
||||||
|
router.post("/check-email", userController.checkEmail)
|
||||||
|
|
||||||
|
router.post("/reset-password", userController.resetPassword)
|
||||||
|
|
||||||
|
// User authentication route
|
||||||
|
router.post("/authenticate", userController.authenticateUser)
|
||||||
|
|
||||||
|
// Update user data route
|
||||||
|
router.put("/update", authenticateToken, userController.updateUserData)
|
||||||
|
|
||||||
|
// Retrieve user details
|
||||||
|
router.get('/retrieveUser', authenticateToken, verifyUser, userController.getUserDetails);
|
||||||
|
|
||||||
|
// EXCLUSIVE ADMIN ACCOUNT
|
||||||
|
// Set user to Admin User
|
||||||
|
router.post('/set-admin', authenticateToken, verifyAdmin, userController.setAdmin);
|
||||||
|
|
||||||
|
router.post('/create-promo-code', authenticateToken, verifyAdmin, userController.createPromoCode);
|
||||||
|
router.delete('/delete-promo-code/:id', authenticateToken, verifyAdmin, userController.deletePromoCode);
|
||||||
|
router.get('/promo-code', authenticateToken, verifyAdmin, userController.showPromoCodes);
|
||||||
|
|
||||||
|
// Address
|
||||||
|
router.post('/get-address', authenticateToken, verifyUser, userController.getAddresses);
|
||||||
|
|
||||||
|
router.post('/create-address', authenticateToken, verifyUser, userController.createAddress);
|
||||||
|
|
||||||
|
router.delete('/delete-address/:id', authenticateToken, userController.deleteAddress);
|
||||||
|
|
||||||
|
router.put('/update-address/:id', authenticateToken, userController.updateAddress);
|
||||||
|
|
||||||
|
module.exports = router
|
After Width: | Height: | Size: 309 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 245 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 331 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 47 KiB |
@ -0,0 +1,2 @@
|
|||||||
|
REACT_APP_API_URL=http://localhost:4000
|
||||||
|
SECRET_SAUCE=cornhub
|
@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
@ -0,0 +1,70 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||||
|
|
||||||
|
The page will reload when you make changes.\
|
||||||
|
You may also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||||
|
|
||||||
|
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||||
|
|
||||||
|
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "csp3-reciproco",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"axios": "^1.6.3",
|
||||||
|
"bootstrap": "^5.3.2",
|
||||||
|
"bootstrap-icons": "^1.11.2",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-bootstrap": "^2.9.1",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-error-boundary": "^4.0.11",
|
||||||
|
"react-icons": "^4.12.0",
|
||||||
|
"react-redux": "^9.0.4",
|
||||||
|
"react-responsive": "^9.0.2",
|
||||||
|
"react-router-dom": "^6.20.1",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"react-slick": "^0.29.0",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"sass": "^1.69.5",
|
||||||
|
"slick-carousel": "^1.8.1",
|
||||||
|
"typeface-montserrat": "^1.1.13",
|
||||||
|
"typeface-roboto": "^1.1.13",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"prestart": "sass src/_custom-bootstrap-theme.scss:src/_custom-bootstrap-theme.css --no-source-map"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 289 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.18.0/font/bootstrap-icons.css" integrity="sha384-oXqNBxTFd42y8S9ZXyoUQ5dAQz0x1Sk0zE8PC+R2Q/kT0WllMzBTYM+no2XH2BwF" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 9.4 KiB |
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
@ -0,0 +1,35 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
/* color: #1fcad4; /* Primary color for headlines */
|
||||||
|
} */
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #333; /* Dark gray color for paragraphs */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add this style in your CSS file or style section */
|
||||||
|
.button-style {
|
||||||
|
background-color: #1fcad4; /* Primary color for the button */
|
||||||
|
color: #fff; /* White text color */
|
||||||
|
border: none; /* Remove border */
|
||||||
|
padding: 10px 20px; /* Add padding */
|
||||||
|
font-size: 16px; /* Set font size */
|
||||||
|
cursor: pointer; /* Add a pointer cursor on hover */
|
||||||
|
border-radius: 5px; /* Add a slight border radius */
|
||||||
|
transition: background-color 0.3s ease; /* Smooth transition on background color change */
|
||||||
|
|
||||||
|
/* Hover effect */
|
||||||
|
&:hover {
|
||||||
|
background-color: #149094; /* Darker shade on hover */
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
// import { Container } from 'react-bootstrap';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import NavBar from './components/NavBar';
|
||||||
|
import Home from './pages/Home';
|
||||||
|
import About from './pages/About';
|
||||||
|
import Login from './pages/Login';
|
||||||
|
import Logout from './pages/Logout';
|
||||||
|
import Profile from './pages/Profile';
|
||||||
|
import Register from './pages/Register';
|
||||||
|
import CartPage from './pages/CartPage';
|
||||||
|
import TokenExpired from './pages/TokenExpired';
|
||||||
|
import TokenChecker from './components/TokenChecker';
|
||||||
|
import Banner from './components/Banner';
|
||||||
|
import AnnouncementBar from './components/Announcement-bar';
|
||||||
|
import UpdateProfile from './components/UpdateProfile';
|
||||||
|
import AdminDashboard from './pages/AdminDashboard';
|
||||||
|
import ProductPage from './pages/ProductPage';
|
||||||
|
import CheckoutPage from './pages/CheckoutPage';
|
||||||
|
import Error from './pages/Error'; // Import the NotFound component
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import { UserProvider } from './UserContext';
|
||||||
|
|
||||||
|
function ErrorFallback({ error }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Something went wrong!</h1>
|
||||||
|
<p>{error.message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [user, setUser] = useState({
|
||||||
|
id: null,
|
||||||
|
isAdmin: null,
|
||||||
|
isVerified: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsetUser = () => {
|
||||||
|
localStorage.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkTokenExpiration = async () => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/user/details`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (typeof data._id !== 'undefined') {
|
||||||
|
setUser({
|
||||||
|
id: data._id,
|
||||||
|
isAdmin: data.isAdmin,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setUser({
|
||||||
|
id: null,
|
||||||
|
isAdmin: null,
|
||||||
|
});
|
||||||
|
unsetUser();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle non-OK responses
|
||||||
|
console.error('Non-OK response:', response.status, response.statusText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Handle fetch errors
|
||||||
|
console.error('Fetch error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
checkTokenExpiration();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserProvider value={{ user, setUser, unsetUser }}>
|
||||||
|
<Router>
|
||||||
|
<TokenChecker />
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
|
<AnnouncementBar />
|
||||||
|
<NavBar />
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/token-expired" element={<TokenExpired />} />
|
||||||
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/dashboard" element={<AdminDashboard />} />
|
||||||
|
<Route path="/profile" element={<Profile />} />
|
||||||
|
<Route path="/logout" element={<Logout />} />
|
||||||
|
<Route path="/about" element={<About />} />
|
||||||
|
<Route path="/banner" element={<Banner />} />
|
||||||
|
<Route path="/cart" element={<CartPage />} />
|
||||||
|
<Route path="/update-profile" element={<UpdateProfile />} />
|
||||||
|
<Route path="/404" element={<Error />} />
|
||||||
|
<Route path="/product/:productId" element={<ProductPage />} />
|
||||||
|
<Route path="/checkout" element={<CheckoutPage />} />
|
||||||
|
{/* Catch-all route that redirects to the home page */}
|
||||||
|
<Route path="*" element={<Navigate to="/404" />} />
|
||||||
|
</Routes>
|
||||||
|
<ToastContainer />
|
||||||
|
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Router>
|
||||||
|
</UserProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
@ -0,0 +1,122 @@
|
|||||||
|
import React, { createContext, useContext, useReducer, useEffect } from "react";
|
||||||
|
|
||||||
|
// Define the initial state and reducer
|
||||||
|
const initialState = {
|
||||||
|
cart: [],
|
||||||
|
cartDetails: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cartReducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "SET_CART":
|
||||||
|
return { ...state, cart: action.payload };
|
||||||
|
|
||||||
|
case "SET_CART_DETAILS":
|
||||||
|
return { ...state, cartDetails: action.payload };
|
||||||
|
|
||||||
|
case "CLEAR_CART":
|
||||||
|
return { ...state, cart: [], cartDetails: null };
|
||||||
|
|
||||||
|
// Add other cases as needed
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the context
|
||||||
|
const CartContext = createContext();
|
||||||
|
|
||||||
|
// Create the provider component
|
||||||
|
export const CartProvider = ({ children }) => {
|
||||||
|
const [state, dispatch] = useReducer(cartReducer, initialState);
|
||||||
|
|
||||||
|
// Action to set the cart data
|
||||||
|
const setCart = (cartData) => {
|
||||||
|
dispatch({ type: "SET_CART", payload: cartData });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action to set cart details
|
||||||
|
const setCartDetails = (details) => {
|
||||||
|
dispatch({ type: "SET_CART_DETAILS", payload: details });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action to clear the cart
|
||||||
|
const clearCart = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/cart/clear-cart`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log("Cart cleared successfully.");
|
||||||
|
setCart([]); // Clear the cart locally
|
||||||
|
setCartDetails(null); // Clear the cart details locally
|
||||||
|
} else {
|
||||||
|
console.error("Failed to clear cart.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error clearing cart:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch cart details from the server
|
||||||
|
const fetchCartDetails = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/cart/cart-details`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setCart(data.items);
|
||||||
|
setCartDetails(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching cart details:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch cart details when the component mounts or when the user changes
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token) {
|
||||||
|
fetchCartDetails();
|
||||||
|
}
|
||||||
|
}, []); // The empty dependency array ensures that this effect runs only once when the component mounts
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CartContext.Provider
|
||||||
|
value={{ state, setCart, setCartDetails, fetchCartDetails, clearCart }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CartContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom hook to use the cart context
|
||||||
|
export const useCart = () => {
|
||||||
|
const context = useContext(CartContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCart must be used within a CartProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Creates a Context object
|
||||||
|
// A context object as the name states is a data type of an object that can be used to store information that can be shared to other components within the app
|
||||||
|
// The context object is a different approach to passing information between components and allows easier access by avoiding the use of prop-drilling
|
||||||
|
const UserContext = React.createContext();
|
||||||
|
|
||||||
|
// The "Provider" component allows other components to consume/use the context object and supply the necessary information needed to the context object
|
||||||
|
export const UserProvider = UserContext.Provider;
|
||||||
|
|
||||||
|
export default UserContext;
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1,4 @@
|
|||||||
|
// src/_custom-bootstrap-theme.scss
|
||||||
|
|
||||||
|
// $primary: #1fcad4; // Your primary color
|
||||||
|
// $secondary: #f5f5f5; // Your secondary color
|
@ -0,0 +1,15 @@
|
|||||||
|
// src/_custom-styles.scss
|
||||||
|
|
||||||
|
// Your custom styles go here
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add more styles as needed
|
||||||
|
|
@ -0,0 +1,86 @@
|
|||||||
|
// AddProductForm.js
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Form, Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const AddProductForm = ({ onSave }) => {
|
||||||
|
const [product, setProduct] = useState({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
price: 0,
|
||||||
|
tags: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setProduct((prevProduct) => ({
|
||||||
|
...prevProduct,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(product);
|
||||||
|
setProduct({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
price: 0,
|
||||||
|
tags: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<Form.Group controlId="formName">
|
||||||
|
<Form.Label>Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter product name"
|
||||||
|
name="name"
|
||||||
|
value={product.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="formDescription">
|
||||||
|
<Form.Label>Description</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
as="textarea"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Enter product description"
|
||||||
|
name="description"
|
||||||
|
value={product.description}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="formPrice">
|
||||||
|
<Form.Label>Price</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter product price"
|
||||||
|
name="price"
|
||||||
|
value={product.price}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="formTags">
|
||||||
|
<Form.Label>Tags</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter product tags"
|
||||||
|
name="tags"
|
||||||
|
value={product.tags}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Button variant="primary" onClick={handleSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddProductForm;
|
@ -0,0 +1,178 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Form, Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
|
||||||
|
const AddressForm = ({
|
||||||
|
selectedAddress,
|
||||||
|
setSelectedAddress,
|
||||||
|
handleContinueAddress,
|
||||||
|
addressFormError,
|
||||||
|
}) => {
|
||||||
|
const philippineStates = [
|
||||||
|
"...",
|
||||||
|
"Abra",
|
||||||
|
"Agusan del Norte",
|
||||||
|
"Agusan del Sur",
|
||||||
|
"Aklan",
|
||||||
|
"Albay",
|
||||||
|
"Antique",
|
||||||
|
"Apayao",
|
||||||
|
"Aurora",
|
||||||
|
"Basilan",
|
||||||
|
"Bataan",
|
||||||
|
"Batanes",
|
||||||
|
"Batangas",
|
||||||
|
"Benguet",
|
||||||
|
"Biliran",
|
||||||
|
"Bohol",
|
||||||
|
"Bukidnon",
|
||||||
|
"Bulacan",
|
||||||
|
"Cagayan",
|
||||||
|
"Camarines Norte",
|
||||||
|
"Camarines Sur",
|
||||||
|
"Camiguin",
|
||||||
|
"Capiz",
|
||||||
|
"Catanduanes",
|
||||||
|
"Cavite",
|
||||||
|
"Cebu",
|
||||||
|
"Cotabato",
|
||||||
|
"Davao de Oro (formerly Compostela Valley)",
|
||||||
|
"Davao del Norte",
|
||||||
|
"Davao del Sur",
|
||||||
|
"Davao Occidental",
|
||||||
|
"Davao Oriental",
|
||||||
|
"Dinagat Islands",
|
||||||
|
"Eastern Samar",
|
||||||
|
"Guimaras",
|
||||||
|
"Ifugao",
|
||||||
|
"Ilocos Norte",
|
||||||
|
"Ilocos Sur",
|
||||||
|
"Iloilo",
|
||||||
|
"Isabela",
|
||||||
|
"Kalinga",
|
||||||
|
"La Union",
|
||||||
|
"Laguna",
|
||||||
|
"Lanao del Norte",
|
||||||
|
"Lanao del Sur",
|
||||||
|
"Leyte",
|
||||||
|
"Maguindanao",
|
||||||
|
"Marinduque",
|
||||||
|
"Masbate",
|
||||||
|
"Misamis Occidental",
|
||||||
|
"Misamis Oriental",
|
||||||
|
"Mountain Province",
|
||||||
|
"Negros Occidental",
|
||||||
|
"Negros Oriental",
|
||||||
|
"Northern Samar",
|
||||||
|
"Nueva Ecija",
|
||||||
|
"Nueva Vizcaya",
|
||||||
|
"Occidental Mindoro",
|
||||||
|
"Oriental Mindoro",
|
||||||
|
"Palawan",
|
||||||
|
"Pampanga",
|
||||||
|
"Pangasinan",
|
||||||
|
"Quezon",
|
||||||
|
"Quirino",
|
||||||
|
"Rizal",
|
||||||
|
"Romblon",
|
||||||
|
"Samar (Western Samar)",
|
||||||
|
"Sarangani",
|
||||||
|
"Siquijor",
|
||||||
|
"Sorsogon",
|
||||||
|
"South Cotabato",
|
||||||
|
"Southern Leyte",
|
||||||
|
"Sultan Kudarat",
|
||||||
|
"Sulu",
|
||||||
|
"Surigao del Norte",
|
||||||
|
"Surigao del Sur",
|
||||||
|
"Tarlac",
|
||||||
|
"Tawi-Tawi",
|
||||||
|
"Zambales",
|
||||||
|
"Zamboanga del Norte",
|
||||||
|
"Zamboanga del Sur",
|
||||||
|
"Zamboanga Sibugay",
|
||||||
|
];
|
||||||
|
// Define the array of Philippine states
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleContinueAddress}>
|
||||||
|
{/* Address form for adding a new address */}
|
||||||
|
<Form.Group controlId="street">
|
||||||
|
<Form.Label>Street</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter street address"
|
||||||
|
value={selectedAddress?.street || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedAddress({
|
||||||
|
...selectedAddress,
|
||||||
|
street: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="zipCode">
|
||||||
|
<Form.Label>Zip / Postal Code</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter zip/postal code"
|
||||||
|
value={selectedAddress?.zipCode || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedAddress({
|
||||||
|
...selectedAddress,
|
||||||
|
zipCode: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="city">
|
||||||
|
<Form.Label>City</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter city"
|
||||||
|
value={selectedAddress?.city || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedAddress({
|
||||||
|
...selectedAddress,
|
||||||
|
city: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="state">
|
||||||
|
<Form.Label>State</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
as="select"
|
||||||
|
value={selectedAddress?.state || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedAddress({
|
||||||
|
...selectedAddress,
|
||||||
|
state: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{philippineStates.map((state, index) => (
|
||||||
|
<option key={index}>{state}</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="country">
|
||||||
|
<Form.Label>Country</Form.Label>
|
||||||
|
<Form.Control type="text" placeholder="Philippines" disabled />
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Button className="mt-3" variant="primary" type="submit">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
{addressFormError && (
|
||||||
|
<div className="text-danger mt-2">{addressFormError}</div>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddressForm;
|
@ -0,0 +1,516 @@
|
|||||||
|
// src/components/AllProducts.js
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useContext, useRef } from "react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Spinner,
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Dropdown,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import { ToastContainer, toast } from "react-toastify";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
import UserContext from "../UserContext";
|
||||||
|
|
||||||
|
const AllProducts = React.forwardRef((props, ref) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const [products, setProducts] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [productIdToDelete, setProductIdToDelete] = useState(null);
|
||||||
|
const [refreshProducts, setRefreshProducts] = useState(false);
|
||||||
|
const [filter, setFilter] = useState("all"); // 'all', 'available', 'notAvailable'
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 4;
|
||||||
|
const prevRefreshRef = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
prevRefreshRef.current = refreshProducts;
|
||||||
|
}, [refreshProducts]);
|
||||||
|
|
||||||
|
const refreshRef = useRef();
|
||||||
|
refreshRef.current = refreshProducts;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
try {
|
||||||
|
if (user && user.id) {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/all`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setProducts(data);
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
setError("Failed to fetch products");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching products:", error);
|
||||||
|
setError("An unexpected error occurred");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchProducts();
|
||||||
|
}, [user, refreshRef.current]);
|
||||||
|
|
||||||
|
const handleDelete = async (productId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/delete/${productId}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setProducts((prevProducts) =>
|
||||||
|
prevProducts.filter((product) => product._id !== productId)
|
||||||
|
);
|
||||||
|
setError(null);
|
||||||
|
toast.success("Product deleted successfully", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setRefreshProducts((prev) => !prev);
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
setError(data.message || "Failed to delete product");
|
||||||
|
toast.error("Failed to delete product");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting product:", error);
|
||||||
|
setError("An unexpected error occurred");
|
||||||
|
toast.error("An unexpected error occurred");
|
||||||
|
} finally {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetAvailability = async (productId, isActive) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const endpoint = isActive ? "activate" : "archive";
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/${productId}/${endpoint}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setProducts((prevProducts) =>
|
||||||
|
prevProducts.map((product) =>
|
||||||
|
product._id === productId ? { ...product, isActive } : product
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setError(null);
|
||||||
|
toast.success(
|
||||||
|
`Product ${isActive ? "activated" : "archived"} successfully`,
|
||||||
|
{
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setRefreshProducts((prev) => !prev);
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
setError(
|
||||||
|
data.message ||
|
||||||
|
`Failed to ${isActive ? "activate" : "archive"} product`
|
||||||
|
);
|
||||||
|
toast.error(`Failed to ${isActive ? "activate" : "archive"} product`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error ${isActive ? "activating" : "archiving"} product:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
setError("An unexpected error occurred");
|
||||||
|
toast.error("An unexpected error occurred");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeleteModal = (productId) => {
|
||||||
|
setProductIdToDelete(productId);
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDeleteModal = () => {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setProductIdToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = (selectedFilter) => {
|
||||||
|
setFilter(selectedFilter);
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredProducts = () => {
|
||||||
|
if (filter === "available") {
|
||||||
|
return products.filter((product) => product.isActive);
|
||||||
|
} else if (filter === "notAvailable") {
|
||||||
|
return products.filter((product) => !product.isActive);
|
||||||
|
} else {
|
||||||
|
return products;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredProducts().length / itemsPerPage);
|
||||||
|
|
||||||
|
const paginatedProducts = () => {
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const filtered = filteredProducts();
|
||||||
|
console.log("Filtered Products:", filtered); // Add this line for debugging
|
||||||
|
return filtered.slice(startIndex, endIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextPage = () => {
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
setCurrentPage((prevPage) => prevPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevPage = () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
setCurrentPage((prevPage) => prevPage - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [editedProduct, setEditedProduct] = useState(null);
|
||||||
|
|
||||||
|
const openEditModal = (product) => {
|
||||||
|
setEditedProduct(product);
|
||||||
|
setShowEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditModal = () => {
|
||||||
|
setEditedProduct(null);
|
||||||
|
setShowEditModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (product) => {
|
||||||
|
openEditModal(product);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveEdit = async () => {
|
||||||
|
// Add logic to save the edited product
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/edit/${editedProduct._id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(editedProduct),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Update the state with the edited product
|
||||||
|
setProducts((prevProducts) =>
|
||||||
|
prevProducts.map((p) =>
|
||||||
|
p._id === editedProduct._id ? editedProduct : p
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setError(null);
|
||||||
|
toast.success("Product edited successfully", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setRefreshProducts((prev) => !prev);
|
||||||
|
closeEditModal();
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
setError(data.message || "Failed to edit product");
|
||||||
|
toast.error("Failed to edit product");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error editing product:", error);
|
||||||
|
setError("An unexpected error occurred");
|
||||||
|
toast.error("An unexpected error occurred");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
refreshProducts: async () => {
|
||||||
|
try {
|
||||||
|
setRefreshProducts((prev) => !prev);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing products:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Spinner animation="border" role="status">
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</Spinner>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Alert variant="danger">{error}</Alert>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2>Product List</h2>
|
||||||
|
<Dropdown onSelect={(selectedFilter) => handleFilterChange(selectedFilter)}>
|
||||||
|
<Dropdown.Toggle variant="success" id="dropdown-basic">
|
||||||
|
Filter
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item eventKey="all">All Products</Dropdown.Item>
|
||||||
|
<Dropdown.Item eventKey="available">Available</Dropdown.Item>
|
||||||
|
<Dropdown.Item eventKey="notAvailable">Not Available</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
{paginatedProducts().map((product, index) => (
|
||||||
|
<div key={product._id} className={`col-md-6 mb-3${index % 2 === 0 ? ' pr-md-2' : ' pl-md-2'}`}>
|
||||||
|
<Table striped bordered hover responsive>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colSpan="2">Product Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Image</td>
|
||||||
|
<td>
|
||||||
|
{product.image ? (
|
||||||
|
<a
|
||||||
|
href={product.image}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
View Image
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"No Link"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td className="text-truncate" style={{ maxWidth: '150px' }}>{product.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Description</td>
|
||||||
|
<td className="text-truncate" style={{ maxWidth: '150px' }}>{product.description}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Price</td>
|
||||||
|
<td>{product.price}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>{product.tags}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Availability</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className={product.isActive ? "text-success" : "text-danger"}
|
||||||
|
>
|
||||||
|
{product.isActive ? "Available" : "Not Available"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Action</td>
|
||||||
|
<td>
|
||||||
|
<Button variant="info" onClick={() => handleEdit(product)}>
|
||||||
|
Edit
|
||||||
|
</Button>{" "}
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => openDeleteModal(product._id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Set Availability</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
variant={product.isActive ? "danger" : "success"}
|
||||||
|
onClick={() =>
|
||||||
|
handleSetAvailability(product._id, !product.isActive)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{product.isActive ? "Archive" : "Activate"}
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<div>
|
||||||
|
Page {currentPage} of {totalPages}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handlePrevPage}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>{" "}
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleNextPage}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal show={showEditModal} onHide={closeEditModal} centered>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>Edit Product</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<form>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="productName">Name:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="productName"
|
||||||
|
className="form-control"
|
||||||
|
value={editedProduct?.name || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditedProduct((prev) => ({ ...prev, name: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="productDescription">Description:</label>
|
||||||
|
<textarea
|
||||||
|
id="productDescription"
|
||||||
|
className="form-control"
|
||||||
|
value={editedProduct?.description || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditedProduct((prev) => ({ ...prev, description: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="productPrice">Price:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="productPrice"
|
||||||
|
className="form-control"
|
||||||
|
value={editedProduct?.price || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditedProduct((prev) => ({ ...prev, price: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="productTags">Tags:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="productTags"
|
||||||
|
className="form-control"
|
||||||
|
value={editedProduct?.tags || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditedProduct((prev) => ({ ...prev, tags: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Add other form fields as needed */}
|
||||||
|
</form>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="secondary" onClick={closeEditModal}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={handleSaveEdit}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
|
||||||
|
<Modal show={showDeleteModal} onHide={closeDeleteModal}>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>Delete Confirmation</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>Are you sure you want to delete this product?</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="secondary" onClick={closeDeleteModal}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => handleDelete(productIdToDelete)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AllProducts;
|
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import 'bootstrap-icons/font/bootstrap-icons.css';
|
||||||
|
|
||||||
|
const AnnouncementBar = () => {
|
||||||
|
return (
|
||||||
|
<div className="alert alert-info mb-0 text-center">
|
||||||
|
Announcement: <strong>GET 15% OFF ON YOUR FIRST PURCHASE! USE CODE: NEW15 </strong>
|
||||||
|
<i class="bi bi-gift-fill mx-1"></i>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnnouncementBar;
|
@ -0,0 +1,49 @@
|
|||||||
|
// src/components/Banner.js
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Carousel, Row, Col } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const Banner = ({ data }) => {
|
||||||
|
const { images } = data;
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
|
||||||
|
const handleSelect = (selectedIndex, e) => {
|
||||||
|
setIndex(selectedIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
setIndex((prevIndex) => (prevIndex + 1) % images.length);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [images.length]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Carousel
|
||||||
|
activeIndex={index}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
interval={null}
|
||||||
|
className="carousel-slide"
|
||||||
|
>
|
||||||
|
{images.map((image, idx) => (
|
||||||
|
<Carousel.Item key={idx}>
|
||||||
|
<img
|
||||||
|
className="d-block w-100"
|
||||||
|
src={image.src}
|
||||||
|
alt={`Slide ${idx}`}
|
||||||
|
style={{ height: 'auto', objectFit: 'contain' }} // Adjust height and styling here
|
||||||
|
/>
|
||||||
|
<Carousel.Caption>
|
||||||
|
<Row className="justify-content-center align-items-center h-100">
|
||||||
|
<Col className="text-center">
|
||||||
|
{/* Your caption content */}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Carousel.Caption>
|
||||||
|
</Carousel.Item>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Banner;
|
@ -0,0 +1,162 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Form, Button } from 'react-bootstrap';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
|
const CreateProduct = () => {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [price, setPrice] = useState('');
|
||||||
|
const [image, setImage] = useState(null);
|
||||||
|
const [isActive, setIsActive] = useState(true);
|
||||||
|
const [tags, setTags] = useState([]); // Changed to array for tags
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleImageChange = (e) => {
|
||||||
|
const selectedImage = e.target.files[0];
|
||||||
|
setImage(selectedImage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', name);
|
||||||
|
formData.append('description', description);
|
||||||
|
formData.append('price', price);
|
||||||
|
formData.append('tags', JSON.stringify(tags)); // Convert tags array to JSON string
|
||||||
|
formData.append('isActive', isActive);
|
||||||
|
formData.append('image', image);
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/create`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success('Product created successfully', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect to AllProducts page
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to create product. Please check your input and try again.', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating product:', error);
|
||||||
|
|
||||||
|
toast.error('An unexpected error occurred. Please try again later.', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<h1 className="my-5 text-center">Create Product</h1>
|
||||||
|
|
||||||
|
<Form.Group controlId="productName">
|
||||||
|
<Form.Label>Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter product name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="productDescription">
|
||||||
|
<Form.Label>Description</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
as="textarea"
|
||||||
|
placeholder="Enter product description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="productPrice">
|
||||||
|
<Form.Label>Price</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter product price"
|
||||||
|
value={price}
|
||||||
|
onChange={(e) => setPrice(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="productTag">
|
||||||
|
<Form.Label>Tag</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter product tag"
|
||||||
|
value={tags.join(', ')} // Display tags as a comma-separated string
|
||||||
|
onChange={(e) => setTags(e.target.value.split(',').map(tag => tag.trim()))} // Convert string to array
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="productStatus">
|
||||||
|
<Form.Check
|
||||||
|
type="checkbox"
|
||||||
|
label="Set Product to Active"
|
||||||
|
checked={isActive}
|
||||||
|
onChange={() => setIsActive(!isActive)}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="productImage">
|
||||||
|
<Form.Label>Image</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
name="image"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Creating...' : 'Create Product'}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateProduct;
|
@ -0,0 +1,222 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Form, Button, Table, Alert, Modal } from "react-bootstrap";
|
||||||
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
|
||||||
|
const CreatePromoCode = () => {
|
||||||
|
const [promoCodes, setPromoCodes] = useState([]);
|
||||||
|
const [promoCode, setPromoCode] = useState("");
|
||||||
|
const [discountAmount, setDiscountAmount] = useState(""); // Change variable name here
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [promoToDelete, setPromoToDelete] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPromoCodes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchPromoCodes = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/user/promo-code`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setPromoCodes(data.promoCodes);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch promo codes:", response.status);
|
||||||
|
setError("Failed to fetch promo codes. Please try again.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching promo codes:", error);
|
||||||
|
setError("An error occurred while fetching promo codes.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const notifySuccess = (message) => {
|
||||||
|
toast.success(message, {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const notifyError = (message) => {
|
||||||
|
toast.error(message, {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreatePromoCode = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/user/create-promo-code`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: promoCode, // Provide the 'code' field
|
||||||
|
discountAmount,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setPromoCode("");
|
||||||
|
setDiscountAmount(""); // Change variable name here
|
||||||
|
setError(null);
|
||||||
|
fetchPromoCodes();
|
||||||
|
notifySuccess("Promo code created successfully");
|
||||||
|
} else {
|
||||||
|
console.error("Failed to create promo code:", response.status);
|
||||||
|
setError("Failed to create promo code. Please try again.");
|
||||||
|
notifyError("Failed to create promo code. Please try again.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating promo code:", error);
|
||||||
|
setError("An error occurred while creating promo code.");
|
||||||
|
notifyError("An error occurred while creating promo code.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDeleteModal = () => {
|
||||||
|
setPromoToDelete(null);
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePromoCode = async (id) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/user/delete-promo-code/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
fetchPromoCodes();
|
||||||
|
notifySuccess("Promo code deleted successfully");
|
||||||
|
} else {
|
||||||
|
console.error("Failed to delete promo code:", response.status);
|
||||||
|
setError("Failed to delete promo code. Please try again.");
|
||||||
|
notifyError("Failed to delete promo code. Please try again.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting promo code:", error);
|
||||||
|
setError("An error occurred while deleting promo code.");
|
||||||
|
notifyError("An error occurred while deleting promo code.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
handleCloseDeleteModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowDeleteModal = (id) => {
|
||||||
|
setPromoToDelete(id);
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Create Promo Code</h2>
|
||||||
|
{error && <Alert variant="danger">{error}</Alert>}
|
||||||
|
<Form>
|
||||||
|
<Form.Group controlId="promoCode">
|
||||||
|
<Form.Label>Promo Code</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter promo code"
|
||||||
|
value={promoCode}
|
||||||
|
onChange={(e) => setPromoCode(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="discountAmount">
|
||||||
|
<Form.Label>Discount Amount</Form.Label> {/* Change label here */}
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter discount amount"
|
||||||
|
value={discountAmount}
|
||||||
|
onChange={(e) => setDiscountAmount(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Button variant="primary" onClick={handleCreatePromoCode}>
|
||||||
|
Create Promo Code
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<h2>Promo Codes</h2>
|
||||||
|
{loading && <p>Loading...</p>}
|
||||||
|
<Table striped bordered hover>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Promo Code</th>
|
||||||
|
<th>Discount Amount</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{promoCodes.map((promo) => (
|
||||||
|
<tr key={promo._id}>
|
||||||
|
<td>{promo.code}</td>{" "}
|
||||||
|
{/* Display 'code' instead of 'promoCode' */}
|
||||||
|
<td>{promo.discountAmount}</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => handleShowDeleteModal(promo._id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* Toast Container */}
|
||||||
|
<ToastContainer position="bottom-right" autoClose={3000} />
|
||||||
|
|
||||||
|
<Modal show={showDeleteModal} onHide={handleCloseDeleteModal}>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>Delete Promo Code</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
Are you sure you want to delete this promo code?
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="secondary" onClick={handleCloseDeleteModal}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => handleDeletePromoCode(promoToDelete)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreatePromoCode;
|
@ -0,0 +1,103 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Modal, Button, Form } from "react-bootstrap";
|
||||||
|
|
||||||
|
const EditAddress = ({ show, onHide, onSave, address }) => {
|
||||||
|
const [editedAddress, setEditedAddress] = useState({ ...address });
|
||||||
|
const [updateStatus, setUpdateStatus] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEditedAddress({ ...address });
|
||||||
|
}, [address]);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
console.log(`Updating ${name} to:`, value);
|
||||||
|
setEditedAddress((prevAddress) => ({
|
||||||
|
...prevAddress,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
console.log("Saving edited address:", editedAddress);
|
||||||
|
try {
|
||||||
|
const response = await onSave(editedAddress);
|
||||||
|
if (response.ok) {
|
||||||
|
console.log("Address updated successfully:", editedAddress._id);
|
||||||
|
setUpdateStatus("success");
|
||||||
|
} else {
|
||||||
|
console.error("Failed to update address:", response.status);
|
||||||
|
setUpdateStatus("error");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating address:", error);
|
||||||
|
setUpdateStatus("error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Modal show={show} onHide={onHide}>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>Edit Address</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<Form>
|
||||||
|
<Form.Group controlId="street">
|
||||||
|
<Form.Label>Street</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
name="street"
|
||||||
|
value={editedAddress.street}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="zipCode">
|
||||||
|
<Form.Label>Zip Code</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
name="zipCode"
|
||||||
|
value={editedAddress.zipCode}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="city">
|
||||||
|
<Form.Label>City</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
name="city"
|
||||||
|
value={editedAddress.city}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="state">
|
||||||
|
<Form.Label>State</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
name="state"
|
||||||
|
value={editedAddress.state}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="country">
|
||||||
|
<Form.Label>Country</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
name="country"
|
||||||
|
value={editedAddress.country}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
</Form>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="secondary" onClick={onHide}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={handleSave}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditAddress;
|
@ -0,0 +1,78 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { Container, Row, Col, Form, Button } from 'react-bootstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import UserContext from '../UserContext';
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="bg-dark text-white py-4">
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
{/* Store Information */}
|
||||||
|
<Col md={3}>
|
||||||
|
<h3 className="mb-4">STORE INFORMATION</h3>
|
||||||
|
<p className='text-white'><i className="bi bi-chevron-right"></i> 999F IGOP Building</p>
|
||||||
|
<p className='text-white'><i className="bi bi-chevron-right"></i> +639-123-456-7890</p>
|
||||||
|
<p className='text-white'><i className="bi bi-chevron-right"></i> customercare@istore.com.ph</p>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Our Company */}
|
||||||
|
<Col md={3}>
|
||||||
|
<h3 className="mb-4">OUR COMPANY</h3>
|
||||||
|
<ul className="list-unstyled">
|
||||||
|
<li className='pb-2'><a href="#" className="text-decoration-none text-white"><i className="bi bi-chevron-right"></i> Terms and conditions of use</a></li>
|
||||||
|
<li className='pb-2'><a href="#" className="text-decoration-none text-white"><i className="bi bi-chevron-right"></i> About us</a></li>
|
||||||
|
<li className='pb-2'><a href="#" className="text-decoration-none text-white"><i className="bi bi-chevron-right"></i> Contact us</a></li>
|
||||||
|
<li className='pb-2'><a href="#" className="text-decoration-none text-white"><i className="bi bi-chevron-right"></i> Stores</a></li>
|
||||||
|
</ul>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Your Account */}
|
||||||
|
<Col md={3}>
|
||||||
|
<h3 className="mb-4">YOUR ACCOUNT</h3>
|
||||||
|
<ul className="list-unstyled">
|
||||||
|
<li><a href="#" className="text-decoration-none text-white"><i className="bi bi-chevron-right"></i> Orders</a></li>
|
||||||
|
<li>
|
||||||
|
{user && user.id !== null ? (
|
||||||
|
<a href="/profile" className="text-decoration-none text-white"><i className="bi bi-chevron-right"></i> Profile</a>
|
||||||
|
) : (
|
||||||
|
<a href="/login" className="text-decoration-none text-white"><i className="bi bi-chevron-right"></i> Login</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Newsletter */}
|
||||||
|
<Col md={3}>
|
||||||
|
<h3 className="mb-4">NEWSLETTER</h3>
|
||||||
|
<Form>
|
||||||
|
<Form.Group controlId="emailForm" className="mb-2">
|
||||||
|
<Form.Control type="email" placeholder="Enter your email" />
|
||||||
|
</Form.Group>
|
||||||
|
<Button variant="light" type="submit" className="d-flex align-items-center">
|
||||||
|
<i className="bi bi-chevron-right me-2"></i>Subscribe
|
||||||
|
</Button>
|
||||||
|
<p className="mt-2 text-white">Sign up and get the latest deals, offers & updates from our store. You may unsubscribe at any moment. For that purpose, please find our contact info in the legal notice.</p>
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* Social Media Icons */}
|
||||||
|
<Row className="mt-4 text-center">
|
||||||
|
<Col>
|
||||||
|
<h3>CONNECT WITH US</h3>
|
||||||
|
<p className="mb-0">
|
||||||
|
<a href="#" className="text-white me-3"><i className="bi bi-facebook"></i></a>
|
||||||
|
<a href="#" className="text-white me-3"><i className="bi bi-twitter"></i></a>
|
||||||
|
<a href="#" className="text-white"><i className="bi bi-instagram"></i></a>
|
||||||
|
</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { Navbar, Nav, Container, Form, FormControl, Button } from 'react-bootstrap';
|
||||||
|
import { NavLink, Link } from 'react-router-dom';
|
||||||
|
import UserContext from '../UserContext';
|
||||||
|
|
||||||
|
export default function NavBar() {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
const handleNavLinkClick = () => {
|
||||||
|
const navbarToggle = document.getElementById('basic-navbar-nav');
|
||||||
|
if (navbarToggle) {
|
||||||
|
navbarToggle.classList.remove('show');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar bg="light" expand="lg" className="my-4 shadow-sm">
|
||||||
|
<Container>
|
||||||
|
<Navbar.Brand as={Link} to="" onClick={handleNavLinkClick}>
|
||||||
|
iStore
|
||||||
|
</Navbar.Brand>
|
||||||
|
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||||
|
<Navbar.Collapse id="basic-navbar-nav" onSelect={handleNavLinkClick}>
|
||||||
|
<Nav className="mx-auto">
|
||||||
|
<Form inline className="d-flex">
|
||||||
|
<FormControl type="text" placeholder="Search" className="mr-sm-2" size="sm" />
|
||||||
|
<Button className="ml-2">
|
||||||
|
<i className="bi bi-search"></i>
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Nav>
|
||||||
|
<Nav className="ml-lg-auto">
|
||||||
|
{user && user.id !== null ? (
|
||||||
|
<>
|
||||||
|
<Nav.Link as={NavLink} to="/profile" exact onClick={handleNavLinkClick}>
|
||||||
|
Profile
|
||||||
|
</Nav.Link>
|
||||||
|
<Nav.Link as={NavLink} to="/logout" exact onClick={handleNavLinkClick}>
|
||||||
|
Logout
|
||||||
|
</Nav.Link>
|
||||||
|
<Nav.Link as={NavLink} to="/cart" onClick={handleNavLinkClick}>
|
||||||
|
Cart
|
||||||
|
</Nav.Link>
|
||||||
|
{user.isAdmin && (
|
||||||
|
<Nav.Link as={NavLink} to="/dashboard" exact onClick={handleNavLinkClick}>
|
||||||
|
Admin Dashboard
|
||||||
|
</Nav.Link>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Nav.Link as={NavLink} to="/login" exact onClick={handleNavLinkClick}>
|
||||||
|
Login
|
||||||
|
</Nav.Link>
|
||||||
|
<Nav.Link as={NavLink} to="/register" exact onClick={handleNavLinkClick}>
|
||||||
|
Register
|
||||||
|
</Nav.Link>
|
||||||
|
<Nav.Link as={NavLink} to="/cart" onClick={handleNavLinkClick}>
|
||||||
|
Cart
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Nav>
|
||||||
|
</Navbar.Collapse>
|
||||||
|
</Container>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
// PrivateRoute.js
|
||||||
|
import React from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const PrivateRoute = ({ element, isVerified }) => {
|
||||||
|
return isVerified ? element : <Navigate to="/404" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrivateRoute;
|
@ -0,0 +1,169 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Container, Col, Button, Row } from "react-bootstrap";
|
||||||
|
import Slider from "react-slick";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import "slick-carousel/slick/slick.css";
|
||||||
|
import "slick-carousel/slick/slick-theme.css";
|
||||||
|
import "./productCatalog.css";
|
||||||
|
|
||||||
|
const ProductCatalog = () => {
|
||||||
|
const [taggedProducts, setTaggedProducts] = useState([]);
|
||||||
|
const [activeFilter, setActiveFilter] = useState("onsale");
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 767 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTaggedProducts = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/product-tag?tag=${activeFilter}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
setTaggedProducts(data);
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
setError("Token is invalid. please login.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching tagged products:", error);
|
||||||
|
setError("Error fetching tagged products. Please try again.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTaggedProducts();
|
||||||
|
}, [activeFilter]);
|
||||||
|
|
||||||
|
const handleAddToCart = (productId) => {
|
||||||
|
console.log(`Product with ID ${productId} added to cart`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sliderSettings = {
|
||||||
|
dots: true,
|
||||||
|
infinite: true,
|
||||||
|
speed: 500,
|
||||||
|
slidesToShow: 1,
|
||||||
|
slidesToScroll: 1,
|
||||||
|
autoplay: true,
|
||||||
|
autoplaySpeed: 3000,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="my-5 text-center">
|
||||||
|
<h2>Top Products</h2>
|
||||||
|
|
||||||
|
<div className="mb-3 d-flex justify-content-center">
|
||||||
|
<Button
|
||||||
|
variant={activeFilter === "onsale" ? "primary" : "outline-primary"}
|
||||||
|
onClick={() => setActiveFilter("onsale")}
|
||||||
|
className="rounded-pill px-3 py-2 mx-2 btn-transition"
|
||||||
|
>
|
||||||
|
On Sale
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
activeFilter === "bestselling" ? "primary" : "outline-primary"
|
||||||
|
}
|
||||||
|
onClick={() => setActiveFilter("bestselling")}
|
||||||
|
className="rounded-pill px-3 py-2 mx-2 btn-transition"
|
||||||
|
>
|
||||||
|
Best Selling
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isMobile ? (
|
||||||
|
<Slider {...sliderSettings}>
|
||||||
|
{error ? (
|
||||||
|
<div>
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
taggedProducts.map((product) => (
|
||||||
|
<div key={product._id}>
|
||||||
|
<img
|
||||||
|
src={product.image || "placeholder-image-url"}
|
||||||
|
alt={product.name}
|
||||||
|
className="img-fluid rounded"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="mb-4"
|
||||||
|
style={{ marginTop: "-70px", marginBottom: "-100px" }}
|
||||||
|
>
|
||||||
|
<h6>{product.name}</h6>
|
||||||
|
<p>
|
||||||
|
{new Intl.NumberFormat("en-PH", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "PHP",
|
||||||
|
}).format(product.price)}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
activeFilter === "bestselling"
|
||||||
|
? "primary"
|
||||||
|
: "outline-primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleAddToCart(product._id)}
|
||||||
|
className="rounded-pill px-3 py-2 btn-transition"
|
||||||
|
>
|
||||||
|
Add to Cart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Slider>
|
||||||
|
) : (
|
||||||
|
<Row>
|
||||||
|
{error ? (
|
||||||
|
<Col>
|
||||||
|
<p>{error}</p>
|
||||||
|
</Col>
|
||||||
|
) : (
|
||||||
|
taggedProducts.map((product) => (
|
||||||
|
<Col key={product._id} className="mb-2">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={product.image || "placeholder-image-url"}
|
||||||
|
alt={product.name}
|
||||||
|
className="img-fluid rounded"
|
||||||
|
/>
|
||||||
|
<h6>{product.name}</h6>
|
||||||
|
<p>
|
||||||
|
{new Intl.NumberFormat("en-PH", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "PHP",
|
||||||
|
}).format(product.price)}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
activeFilter === "bestselling"
|
||||||
|
? "primary"
|
||||||
|
: "outline-primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleAddToCart(product._id)}
|
||||||
|
className="rounded-pill px-3 py-2 btn-transition"
|
||||||
|
>
|
||||||
|
Add to Cart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductCatalog;
|
@ -0,0 +1,163 @@
|
|||||||
|
// ProductCatalog.js
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Container, Col, Button, Row } from 'react-bootstrap';
|
||||||
|
import Slider from 'react-slick';
|
||||||
|
import { useMediaQuery } from 'react-responsive';
|
||||||
|
// import { toast, ToastContainer } from 'react-toastify';
|
||||||
|
import { Link } from 'react-router-dom'; // Import Link from react-router-dom
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import 'slick-carousel/slick/slick.css';
|
||||||
|
import 'slick-carousel/slick/slick-theme.css';
|
||||||
|
import './productCatalog.css';
|
||||||
|
// import { jwtDecode } from 'jwt-decode';
|
||||||
|
|
||||||
|
const ProductCatalog = () => {
|
||||||
|
const [taggedProducts, setTaggedProducts] = useState([]);
|
||||||
|
const [activeFilter, setActiveFilter] = useState('onsale');
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 767 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTaggedProducts = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/product-tag?tag=${activeFilter}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
setTaggedProducts(data);
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
setError('Token is invalid. please login.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching tagged products:', error);
|
||||||
|
setError('Error fetching tagged products. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTaggedProducts();
|
||||||
|
}, [activeFilter]);
|
||||||
|
|
||||||
|
const sliderSettings = {
|
||||||
|
dots: true,
|
||||||
|
infinite: true,
|
||||||
|
speed: 500,
|
||||||
|
slidesToShow: 1,
|
||||||
|
slidesToScroll: 1,
|
||||||
|
autoplay: true,
|
||||||
|
autoplaySpeed: 3000,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="my-5 text-center">
|
||||||
|
<h2>Top Products</h2>
|
||||||
|
|
||||||
|
<div className="mb-3 d-flex justify-content-center">
|
||||||
|
<Button
|
||||||
|
variant={activeFilter === 'onsale' ? 'primary' : 'outline-primary'}
|
||||||
|
onClick={() => setActiveFilter('onsale')}
|
||||||
|
className="rounded-pill px-3 py-2 mx-2 btn-transition"
|
||||||
|
>
|
||||||
|
On Sale
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={activeFilter === 'bestselling' ? 'primary' : 'outline-primary'}
|
||||||
|
onClick={() => setActiveFilter('bestselling')}
|
||||||
|
className="rounded-pill px-3 py-2 mx-2 btn-transition"
|
||||||
|
>
|
||||||
|
Best Selling
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isMobile ? (
|
||||||
|
<Slider {...sliderSettings}>
|
||||||
|
{error ? (
|
||||||
|
<div>
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
taggedProducts.map((product) => (
|
||||||
|
<div key={product._id}>
|
||||||
|
{/* Use Link to make product name clickable */}
|
||||||
|
<Link
|
||||||
|
to={`/product/${product._id}`}
|
||||||
|
className="custom-link d-block text-decoration-none text-dark"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={product.image || 'placeholder-image-url'}
|
||||||
|
alt={product.name}
|
||||||
|
className="img-fluid rounded"
|
||||||
|
/>
|
||||||
|
<div className="mb-4" style={{ marginTop: '-70px', marginBottom: '-100px' }}>
|
||||||
|
<h6>{product.name}</h6>
|
||||||
|
<p>
|
||||||
|
{new Intl.NumberFormat('en-PH', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'PHP',
|
||||||
|
}).format(product.price)}
|
||||||
|
</p>
|
||||||
|
{/* <Button
|
||||||
|
variant={activeFilter === 'bestselling' ? 'primary' : 'outline-primary'}
|
||||||
|
onClick={() => handleAddToCart(product._id)}
|
||||||
|
className="rounded-pill px-3 py-2 btn-transition"
|
||||||
|
>
|
||||||
|
Add to Cart
|
||||||
|
</Button> */}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Slider>
|
||||||
|
) : (
|
||||||
|
<Row>
|
||||||
|
{error ? (
|
||||||
|
<Col>
|
||||||
|
<p>{error}</p>
|
||||||
|
</Col>
|
||||||
|
) : (
|
||||||
|
taggedProducts.map((product) => (
|
||||||
|
<Col key={product._id} className="mb-2">
|
||||||
|
<div>
|
||||||
|
{/* Use Link to make product name clickable */}
|
||||||
|
<Link
|
||||||
|
to={`/product/${product._id}`}
|
||||||
|
className="custom-link d-block text-decoration-none text-dark"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={product.image || 'placeholder-image-url'}
|
||||||
|
alt={product.name}
|
||||||
|
className="img-fluid rounded"
|
||||||
|
/>
|
||||||
|
<h6>{product.name}</h6>
|
||||||
|
<p>
|
||||||
|
{new Intl.NumberFormat('en-PH', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'PHP',
|
||||||
|
}).format(product.price)}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
{/* <ToastContainer /> */}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductCatalog;
|
@ -0,0 +1,49 @@
|
|||||||
|
// components/ProtectedRoute.js
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
|
||||||
|
const isTokenValidFunction = (token) => {
|
||||||
|
try {
|
||||||
|
if (!token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedToken = jwtDecode(token);
|
||||||
|
const currentTime = Date.now() / 1000;
|
||||||
|
return decodedToken.exp > currentTime;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decoding token:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProtectedRoute = ({ element: Element, ...rest }) => {
|
||||||
|
const [isTokenValid, setIsTokenValid] = useState(true);
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkTokenValidity = () => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const isValid = isTokenValidFunction(token);
|
||||||
|
setIsTokenValid(isValid);
|
||||||
|
|
||||||
|
if (!isValid && !location.state?.expired) {
|
||||||
|
navigate('/login', { state: { expired: true } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkTokenValidity();
|
||||||
|
}, [location.state, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
{...rest}
|
||||||
|
element={isTokenValid ? <Element /> : <Navigate to="/login" state={{ expired: true }} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtectedRoute;
|
@ -0,0 +1,155 @@
|
|||||||
|
// src/components/RegisterForm.js
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Form, Button, Container, Row } from 'react-bootstrap';
|
||||||
|
import { toast, ToastContainer } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const RegisterForm = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiUrl = `${process.env.REACT_APP_API_URL}/user/register`;
|
||||||
|
|
||||||
|
// Check if the email already exists
|
||||||
|
const existingUserResponse = await fetch(`${process.env.REACT_APP_API_URL}/user/check-email`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: formData.email }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingUserResult = await existingUserResponse.json();
|
||||||
|
|
||||||
|
if (existingUserResult.exists) {
|
||||||
|
toast.error('This email is already registered. Please use a different email.', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Registration successful
|
||||||
|
toast.success('Registration successful!', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect to the login page
|
||||||
|
navigate('/login');
|
||||||
|
} else {
|
||||||
|
// Registration failed
|
||||||
|
toast.error('Registration failed. Please try again.', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="mt-5 shadow p-5 rounded">
|
||||||
|
<h1 className="text-center mb-4">Register</h1>
|
||||||
|
<Row className="justify-content-center">
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<Form.Group controlId="formEmail">
|
||||||
|
<Form.Label>Email address</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="formPassword">
|
||||||
|
<Form.Label>Password</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{/* <Form.Group controlId="formFirstName">
|
||||||
|
<Form.Label>First Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="First Name"
|
||||||
|
name="firstName"
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="formLastName">
|
||||||
|
<Form.Label>Last Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Last Name"
|
||||||
|
name="lastName"
|
||||||
|
value={formData.lastName}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Form.Group> */}
|
||||||
|
|
||||||
|
<Button variant="primary" type="submit">
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Row>
|
||||||
|
{/* Toast container for notifications */}
|
||||||
|
<ToastContainer position="bottom-right" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterForm;
|
@ -0,0 +1,81 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const ResetPassword = () => {
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
|
const handleResetPassword = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setMessage('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resetToken = // Retrieve the reset token from your URL or state
|
||||||
|
new URLSearchParams(window.location.search).get('resetToken'); // Example: ?resetToken=yourResetToken
|
||||||
|
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/user/reset-password`, {
|
||||||
|
method: 'POST', // Assuming your backend uses POST for reset password
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ newPassword: password, resetToken }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setMessage('Password reset successfully');
|
||||||
|
setPassword('');
|
||||||
|
setConfirmPassword('');
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setMessage(errorData.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('An error occurred. Please try again.');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<h2>Reset Password</h2>
|
||||||
|
<form onSubmit={handleResetPassword}>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="password" className="form-label">
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
id="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="confirmPassword" className="form-label">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
id="confirmPassword"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{message && <div className="alert alert-danger">{message}</div>}
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Reset Password
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPassword;
|
@ -0,0 +1,70 @@
|
|||||||
|
// TokenChecker.js
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const TOKEN_CHECK_INTERVAL = 30000; // 30 seconds in milliseconds
|
||||||
|
|
||||||
|
const TokenChecker = ({ setUser, unsetUser }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const checkTokenExpiration = async () => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const lastCheckTimestamp = localStorage.getItem('lastTokenCheck');
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
// Check if enough time has passed since the last token check
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
if (lastCheckTimestamp && currentTime - lastCheckTimestamp < TOKEN_CHECK_INTERVAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/user/details`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (typeof data._id !== 'undefined') {
|
||||||
|
setUser({
|
||||||
|
id: data._id,
|
||||||
|
isVerified: data.isVerified,
|
||||||
|
isAdmin: data.isAdmin,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setUser({
|
||||||
|
id: null,
|
||||||
|
isVerified: null,
|
||||||
|
isAdmin: null,
|
||||||
|
});
|
||||||
|
unsetUser();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle token expiration by redirecting to the login page or the expired token page
|
||||||
|
window.location.href = '/token-expired'; // Change this line as needed
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during token check:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the last check timestamp in local storage
|
||||||
|
localStorage.setItem('lastTokenCheck', currentTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the token check function
|
||||||
|
checkTokenExpiration();
|
||||||
|
|
||||||
|
// Set up an interval for periodic token checks (adjust as needed)
|
||||||
|
const intervalId = setInterval(checkTokenExpiration, TOKEN_CHECK_INTERVAL);
|
||||||
|
|
||||||
|
// Cleanup: clear the interval when the component is unmounted
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [setUser, unsetUser]);
|
||||||
|
|
||||||
|
// The rest of your component logic goes here...
|
||||||
|
// Note: You don't need a return statement here.
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TokenChecker;
|
@ -0,0 +1,166 @@
|
|||||||
|
// UpdateProfile.js
|
||||||
|
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Form, Button, Alert, Container, Row, Col } from 'react-bootstrap';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import UserContext from '../UserContext';
|
||||||
|
|
||||||
|
export default function UpdateProfile() {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
userId: user.id,
|
||||||
|
newEmail: user.email,
|
||||||
|
newFirstName: user.firstName,
|
||||||
|
newLastName: user.lastName,
|
||||||
|
newPassword: '',
|
||||||
|
newMobileNo: user.mobileNo || '',
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [updateError, setUpdateError] = useState(null);
|
||||||
|
|
||||||
|
const handleUpdateProfile = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/user/update`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const updatedUserData = await response.json();
|
||||||
|
console.log('Updated User Data:', updatedUserData);
|
||||||
|
|
||||||
|
// Handle the updated user data as needed
|
||||||
|
toast.success('Profile updated successfully!', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Failed to update user profile:', response.status);
|
||||||
|
const errorData = await response.json();
|
||||||
|
setUpdateError(errorData.message);
|
||||||
|
toast.error('Failed to update profile. Please try again.', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating user profile:', error);
|
||||||
|
setUpdateError('An unexpected error occurred. Please try again later.');
|
||||||
|
toast.error('An unexpected error occurred. Please try again later.', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update form data when user data changes
|
||||||
|
setFormData({
|
||||||
|
userId: user.id,
|
||||||
|
newEmail: user.email,
|
||||||
|
newFirstName: user.firstName,
|
||||||
|
newLastName: user.lastName,
|
||||||
|
newPassword: '',
|
||||||
|
newMobileNo: user.mobileNo || '',
|
||||||
|
});
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="mt-5 shadow p-5 rounded">
|
||||||
|
<Row className="justify-content-center">
|
||||||
|
<Col lg={6} offset={3}>
|
||||||
|
<Form onSubmit={handleUpdateProfile}>
|
||||||
|
<h1 className="my-5 text-center">Update Profile</h1>
|
||||||
|
|
||||||
|
{updateError && <Alert variant="danger">{updateError}</Alert>}
|
||||||
|
|
||||||
|
<Form.Group controlId="newEmail">
|
||||||
|
<Form.Label>Email address</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter email"
|
||||||
|
value={formData.newEmail}
|
||||||
|
onChange={(e) => setFormData({ ...formData, newEmail: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="newFirstName">
|
||||||
|
<Form.Label className="mt-2">First Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter first name"
|
||||||
|
value={formData.newFirstName}
|
||||||
|
onChange={(e) => setFormData({ ...formData, newFirstName: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="newLastName">
|
||||||
|
<Form.Label className="mt-2">Last Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter last name"
|
||||||
|
value={formData.newLastName}
|
||||||
|
onChange={(e) => setFormData({ ...formData, newLastName: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="newMobileNo">
|
||||||
|
<Form.Label className="mt-2">Mobile Number</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter mobile number"
|
||||||
|
value={formData.newMobileNo}
|
||||||
|
onChange={(e) => setFormData({ ...formData, newMobileNo: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="newPassword">
|
||||||
|
<Form.Label className="mt-2">New Password</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter new password"
|
||||||
|
value={formData.newPassword}
|
||||||
|
onChange={(e) => setFormData({ ...formData, newPassword: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Button className="mt-3" variant="primary" type="submit" disabled>
|
||||||
|
Updating...
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button className="mt-3" variant="primary" type="submit">
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
.banner-content {
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5); /* Background color with low opacity */
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 600px; /* Adjust the max-width as needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* src/components/Banner.css */
|
||||||
|
.carousel-fade .carousel-inner .carousel-item {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 1s ease-in-out; /* Adjust the duration and easing function as needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-fade .carousel-inner .carousel-item.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
.product-container {
|
||||||
|
height: 600px; /* Set your preferred height */
|
||||||
|
width: 100%; /* Adjust as needed */
|
||||||
|
overflow: hidden; /* Hide overflow content */
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left {
|
||||||
|
animation: slideLeft 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right {
|
||||||
|
animation: slideRight 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideLeft {
|
||||||
|
from {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideRight {
|
||||||
|
from {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
|||||||
|
// useCart.js
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const useCart = () => {
|
||||||
|
const [cartState, setCartState] = useState({
|
||||||
|
// ... your initial cart state
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchCartDetails = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/cart/cart-details`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setCartState(data);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch cart details');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching cart details:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
// Assuming you have user context and user.id
|
||||||
|
const userId = ''; // Replace with the actual user.id from your user context
|
||||||
|
|
||||||
|
if (userId && token) {
|
||||||
|
fetchCartDetails(userId);
|
||||||
|
}
|
||||||
|
}, []); // The empty dependency array ensures that this effect runs only once when the component mounts
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: cartState,
|
||||||
|
fetchCartDetails,
|
||||||
|
// ... other functions and state as needed
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useCart };
|
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux'; // Import the Provider
|
||||||
|
import App from './App';
|
||||||
|
import store from './redux/store'; // Import your Redux store
|
||||||
|
import 'typeface-montserrat';
|
||||||
|
import 'typeface-roboto';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import './_custom-bootstrap-theme.scss'; // Import the custom Bootstrap theme
|
||||||
|
import './_custom-styles.scss'; // Import your custom styles
|
||||||
|
import './App.css'; // Import your app-specific styles
|
||||||
|
import 'bootstrap-icons/font/bootstrap-icons.css';
|
||||||
|
import { CartProvider } from './CartContext';
|
||||||
|
|
||||||
|
const root = createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<CartProvider>
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
</CartProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1,8 @@
|
|||||||
|
// src/pages/About.js
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const About = () => {
|
||||||
|
return <div>About Page</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
@ -0,0 +1,36 @@
|
|||||||
|
// AdminDashboard.js
|
||||||
|
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { Tabs, Tab, Container } from 'react-bootstrap';
|
||||||
|
import CreateProduct from '../components/CreateProduct';
|
||||||
|
import AllProducts from '../components/AllProducts';
|
||||||
|
import CreatePromoCode from '../components/CreatePromoCode'; // Import the new component
|
||||||
|
|
||||||
|
const AdminDashboard = () => {
|
||||||
|
const allProductsRef = useRef();
|
||||||
|
|
||||||
|
const prevRefreshRef = () => {
|
||||||
|
if (allProductsRef.current) {
|
||||||
|
allProductsRef.current.refreshProducts();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Tabs defaultActiveKey="view-all" id="admin-dashboard-tabs" onSelect={(key) => key === 'create' && prevRefreshRef()}>
|
||||||
|
<Tab eventKey="view-all" title="View All Products">
|
||||||
|
<AllProducts ref={allProductsRef} />
|
||||||
|
</Tab>
|
||||||
|
<Tab eventKey="create" title="Create Product">
|
||||||
|
<CreateProduct />
|
||||||
|
</Tab>
|
||||||
|
<Tab eventKey="create-promo-code" title="Create Promo Code"> {/* Add the new tab for creating a promo code */}
|
||||||
|
<CreatePromoCode />
|
||||||
|
</Tab>
|
||||||
|
{/* Add more tabs for other actions if needed */}
|
||||||
|
</Tabs>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminDashboard;
|
@ -0,0 +1,360 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Container, Row, Col, Table, Button, Form } from "react-bootstrap";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import { ToastContainer, toast } from "react-toastify";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
|
||||||
|
const CartPage = () => {
|
||||||
|
const [cartItems, setCartItems] = useState([]);
|
||||||
|
const [productDetails, setProductDetails] = useState({});
|
||||||
|
const [totalPrice, setTotalPrice] = useState(0);
|
||||||
|
const [shippingFee, setShippingFee] = useState(0);
|
||||||
|
const [promoCode, setPromoCode] = useState("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const fetchCartItems = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const decoded = jwtDecode(token);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/cart/user/${decoded.userId}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(data.items)) {
|
||||||
|
setCartItems(data.items);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching cart items:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCartItems();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProductDetails = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const productIds = cartItems.map((item) => item.product);
|
||||||
|
|
||||||
|
const promises = productIds.map(async (productId) => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/${productId}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(
|
||||||
|
`Failed to fetch product details for ID ${productId}: ${response.status}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productData = await response.json();
|
||||||
|
return { [productId]: productData };
|
||||||
|
});
|
||||||
|
|
||||||
|
const productDetailsArray = await Promise.all(promises);
|
||||||
|
const productDetailsObject = productDetailsArray.reduce(
|
||||||
|
(acc, product) => ({ ...acc, ...product }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
setProductDetails(productDetailsObject);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching product details:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cartItems.length > 0) {
|
||||||
|
fetchProductDetails();
|
||||||
|
}
|
||||||
|
}, [cartItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const calculateTotalPrice = () => {
|
||||||
|
const totalPrice = cartItems.reduce((total, item) => {
|
||||||
|
const product = productDetails[item.product];
|
||||||
|
if (product) {
|
||||||
|
return total + product.price * item.quantity;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
setTotalPrice(totalPrice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateShippingFee = () => {
|
||||||
|
// Leave it blank for now, add your logic to calculate shipping fee here
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateTotalPrice();
|
||||||
|
calculateShippingFee();
|
||||||
|
}, [cartItems, productDetails]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch cart items from local storage
|
||||||
|
const storedCartItems = localStorage.getItem("cartItems");
|
||||||
|
if (storedCartItems) {
|
||||||
|
setCartItems(JSON.parse(storedCartItems));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch other necessary data or perform any additional logic
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateCart = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const decoded = jwtDecode(token);
|
||||||
|
|
||||||
|
const storeCartInLocalStorage = (cartItems) => {
|
||||||
|
localStorage.setItem("cartItems", JSON.stringify(cartItems));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updates = cartItems.map((item) => ({
|
||||||
|
product: item.product,
|
||||||
|
quantity: item.quantity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/cart/update-cart`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: decoded.userId,
|
||||||
|
updates,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(data.message, {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchCartItems();
|
||||||
|
storeCartInLocalStorage(cartItems);
|
||||||
|
} else {
|
||||||
|
toast.error(data.error || "Error updating cart", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cart:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuantityChange = (productId, newQuantity) => {
|
||||||
|
setCartItems((prevItems) =>
|
||||||
|
prevItems.map((item) =>
|
||||||
|
item.product === productId
|
||||||
|
? { ...item, quantity: parseInt(newQuantity, 10) }
|
||||||
|
: item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProceedToCheckout = () => {
|
||||||
|
// Add logic to handle the checkout process
|
||||||
|
// You can navigate to the checkout page or display a modal for further steps
|
||||||
|
navigate("/checkout");
|
||||||
|
// This is a placeholder, you need to implement the actual checkout logic
|
||||||
|
toast.success("Proceeding to Checkout...", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 2000,
|
||||||
|
hideProgressBar: true,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="my-5 text-center">
|
||||||
|
<h2>Shopping Cart</h2>
|
||||||
|
{cartItems.length === 0 ? (
|
||||||
|
<p>Your cart is empty.</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
{/* Left side - Product Details */}
|
||||||
|
<Col xs={12} md={8}>
|
||||||
|
<h3>Product Details</h3>
|
||||||
|
{cartItems.map((item) => (
|
||||||
|
<Table
|
||||||
|
key={item.product}
|
||||||
|
striped
|
||||||
|
bordered
|
||||||
|
hover
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colSpan="2">
|
||||||
|
<strong>Product Image</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colSpan="2">
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
(productDetails[item.product] &&
|
||||||
|
productDetails[item.product].image) ||
|
||||||
|
"placeholder-image-url"
|
||||||
|
}
|
||||||
|
alt={
|
||||||
|
(productDetails[item.product] &&
|
||||||
|
productDetails[item.product].name) ||
|
||||||
|
"Product Image"
|
||||||
|
}
|
||||||
|
style={{ width: "200px", height: "200px" }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Name</strong>
|
||||||
|
</td>
|
||||||
|
<td>{productDetails[item.product]?.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Description</strong>
|
||||||
|
</td>
|
||||||
|
<td>{productDetails[item.product]?.description}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Price</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>
|
||||||
|
₱{productDetails[item.product]?.price * item.quantity}
|
||||||
|
</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Quantity</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={item.quantity}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleQuantityChange(item.product, e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Right side - Order Summary */}
|
||||||
|
<Col xs={12} md={4}>
|
||||||
|
<h3>Order Summary</h3>
|
||||||
|
<Table striped bordered hover>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Order Summary</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Total Items</td>
|
||||||
|
<td>
|
||||||
|
{cartItems.reduce(
|
||||||
|
(total, item) => total + item.quantity,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Shipping Fee</td>
|
||||||
|
<td>{shippingFee}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Total</td>
|
||||||
|
<td>
|
||||||
|
<strong>₱{totalPrice + shippingFee}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
<div>
|
||||||
|
<Button variant="primary" onClick={updateCart} className="mt-3">
|
||||||
|
Update Cart
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleProceedToCheckout}
|
||||||
|
className="mx-3 mt-3"
|
||||||
|
>
|
||||||
|
Proceed to Checkout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan="2"
|
||||||
|
className="text-start"
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
opacity: "0.5",
|
||||||
|
lineHeight: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="mt-4">
|
||||||
|
We accept cash, COD, bank deposit, credit card, Dragonpay,
|
||||||
|
and Paypal.
|
||||||
|
</p>
|
||||||
|
<p>(Cash on Delivery available within Metro Manila only)</p>
|
||||||
|
<p>Shipped through trusted couriers</p>
|
||||||
|
<p>7 days store replacement w/ warranty</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ToastContainer />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CartPage;
|
@ -0,0 +1,376 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Container, Row, Col, Table, Button, Form } from "react-bootstrap";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import { ToastContainer, toast } from "react-toastify";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
|
||||||
|
const CartPage = () => {
|
||||||
|
const [cartItems, setCartItems] = useState([]);
|
||||||
|
const [productDetails, setProductDetails] = useState({});
|
||||||
|
const [totalPrice, setTotalPrice] = useState(0);
|
||||||
|
const [shippingFee, setShippingFee] = useState(0);
|
||||||
|
const [promoCode, setPromoCode] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const fetchCartItems = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const decoded = token ? jwtDecode(token) : null;
|
||||||
|
const anonymousUserId = localStorage.getItem("anonymousUserId");
|
||||||
|
|
||||||
|
const url =
|
||||||
|
token
|
||||||
|
? `${process.env.REACT_APP_API_URL}/cart/user/${decoded?.userId}`
|
||||||
|
: `${process.env.REACT_APP_API_URL}/cart/anonymous/${anonymousUserId}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: token ? `Bearer ${token}` : undefined,
|
||||||
|
"X-Anonymous-User-ID": !token ? anonymousUserId : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(data.items)) {
|
||||||
|
setCartItems(data.items);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching cart items:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCartItems();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCartItems();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProductDetails = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const productIds = cartItems.map((item) => item.product);
|
||||||
|
|
||||||
|
const promises = productIds.map(async (productId) => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/${productId}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(
|
||||||
|
`Failed to fetch product details for ID ${productId}: ${response.status}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productData = await response.json();
|
||||||
|
return { [productId]: productData };
|
||||||
|
});
|
||||||
|
|
||||||
|
const productDetailsArray = await Promise.all(promises);
|
||||||
|
const productDetailsObject = productDetailsArray.reduce(
|
||||||
|
(acc, product) => ({ ...acc, ...product }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
setProductDetails(productDetailsObject);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching product details:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cartItems.length > 0) {
|
||||||
|
fetchProductDetails();
|
||||||
|
}
|
||||||
|
}, [cartItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const calculateTotalPrice = () => {
|
||||||
|
const totalPrice = cartItems.reduce((total, item) => {
|
||||||
|
const product = productDetails[item.product];
|
||||||
|
if (product) {
|
||||||
|
return total + product.price * item.quantity;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
setTotalPrice(totalPrice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateShippingFee = () => {
|
||||||
|
// Dummy function for calculating shipping fee
|
||||||
|
// Modify as needed based on your business logic
|
||||||
|
return 50;
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateTotalPrice();
|
||||||
|
setShippingFee(calculateShippingFee());
|
||||||
|
}, [cartItems, productDetails]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch cart items from local storage
|
||||||
|
const storedCartItems = localStorage.getItem("cartItems");
|
||||||
|
if (storedCartItems) {
|
||||||
|
setCartItems(JSON.parse(storedCartItems));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch other necessary data or perform any additional logic
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateCart = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const decoded = jwtDecode(token);
|
||||||
|
|
||||||
|
const storeCartInLocalStorage = (cartItems) => {
|
||||||
|
localStorage.setItem("cartItems", JSON.stringify(cartItems));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updates = cartItems.map((item) => ({
|
||||||
|
product: item.product,
|
||||||
|
quantity: item.quantity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/cart/update-cart`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: decoded.userId,
|
||||||
|
updates,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(data.message, {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchCartItems();
|
||||||
|
storeCartInLocalStorage(cartItems);
|
||||||
|
} else {
|
||||||
|
toast.error(data.error || "Error updating cart", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cart:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuantityChange = (productId, newQuantity) => {
|
||||||
|
setCartItems((prevItems) =>
|
||||||
|
prevItems.map((item) =>
|
||||||
|
item.product === productId
|
||||||
|
? { ...item, quantity: parseInt(newQuantity, 10) }
|
||||||
|
: item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProceedToCheckout = () => {
|
||||||
|
// Add logic to handle the checkout process
|
||||||
|
// You can navigate to the checkout page or display a modal for further steps
|
||||||
|
navigate("/checkout");
|
||||||
|
// This is a placeholder, you need to implement the actual checkout logic
|
||||||
|
toast.success("Proceeding to Checkout...", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 2000,
|
||||||
|
hideProgressBar: true,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="my-5 text-center">
|
||||||
|
<h2>Shopping Cart</h2>
|
||||||
|
{cartItems.length === 0 ? (
|
||||||
|
<p>Your cart is empty.</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
{/* Left side - Product Details */}
|
||||||
|
<Col xs={12} md={8}>
|
||||||
|
<h3>Product Details</h3>
|
||||||
|
{cartItems.map((item) => (
|
||||||
|
<Table
|
||||||
|
key={item.product}
|
||||||
|
striped
|
||||||
|
bordered
|
||||||
|
hover
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colSpan="2">
|
||||||
|
<strong>Product Image</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colSpan="2">
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
(productDetails[item.product] &&
|
||||||
|
productDetails[item.product].image) ||
|
||||||
|
"placeholder-image-url"
|
||||||
|
}
|
||||||
|
alt={
|
||||||
|
(productDetails[item.product] &&
|
||||||
|
productDetails[item.product].name) ||
|
||||||
|
"Product Image"
|
||||||
|
}
|
||||||
|
style={{ width: "200px", height: "200px" }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Name</strong>
|
||||||
|
</td>
|
||||||
|
<td>{productDetails[item.product]?.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Description</strong>
|
||||||
|
</td>
|
||||||
|
<td>{productDetails[item.product]?.description}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Price</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>
|
||||||
|
₱{productDetails[item.product]?.price * item.quantity}
|
||||||
|
</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Quantity</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={item.quantity}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleQuantityChange(item.product, e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Right side - Order Summary */}
|
||||||
|
<Col xs={12} md={4}>
|
||||||
|
<h3>Order Summary</h3>
|
||||||
|
<Table striped bordered hover>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Order Summary</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Total Items</td>
|
||||||
|
<td>
|
||||||
|
{cartItems.reduce(
|
||||||
|
(total, item) => total + item.quantity,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Shipping Fee</td>
|
||||||
|
<td>{shippingFee}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Total</td>
|
||||||
|
<td>
|
||||||
|
<strong>₱{totalPrice + shippingFee}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
<div>
|
||||||
|
<Button variant="primary" onClick={updateCart} className="mt-3">
|
||||||
|
Update Cart
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleProceedToCheckout}
|
||||||
|
className="mx-3 mt-3"
|
||||||
|
>
|
||||||
|
Proceed to Checkout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan="2"
|
||||||
|
className="text-start"
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
opacity: "0.5",
|
||||||
|
lineHeight: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="mt-4">
|
||||||
|
We accept cash, COD, bank deposit, credit card, Dragonpay,
|
||||||
|
and Paypal.
|
||||||
|
</p>
|
||||||
|
<p>(Cash on Delivery available within Metro Manila only)</p>
|
||||||
|
<p>Shipped through trusted couriers</p>
|
||||||
|
<p>7 days store replacement w/ warranty</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ToastContainer />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CartPage;
|
@ -0,0 +1,721 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
|
Form,
|
||||||
|
Modal,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import AddressForm from "../components/AddressForm";
|
||||||
|
import EditAddress from "../components/EditAddress";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
|
||||||
|
const CheckoutPage = () => {
|
||||||
|
const [cartItems, setCartItems] = useState([]);
|
||||||
|
const [totalPrice, setTotalPrice] = useState(0);
|
||||||
|
const [shippingFee, setShippingFee] = useState({
|
||||||
|
value: 200,
|
||||||
|
details: "Fixed shipping fee of ₱200",
|
||||||
|
});
|
||||||
|
const [promoCode, setPromoCode] = useState("");
|
||||||
|
const [personalInfoOpen, setPersonalInfoOpen] = useState(true);
|
||||||
|
const [addressOpen, setAddressOpen] = useState(false);
|
||||||
|
const [paymentOpen, setPaymentOpen] = useState(false);
|
||||||
|
const [isCustomerLoggedIn, setIsCustomerLoggedIn] = useState(false);
|
||||||
|
const [userDetails, setUserDetails] = useState(null);
|
||||||
|
const [appliedPromoCode, setAppliedPromoCode] = useState(null);
|
||||||
|
const [productDetails, setProductDetails] = useState({});
|
||||||
|
const [discountAmount, setDiscountAmount] = useState(0);
|
||||||
|
const [addAddressFormOpen, setAddAddressFormOpen] = useState(false);
|
||||||
|
const [addresses, setAddresses] = useState([]);
|
||||||
|
const [selectedAddressId, setSelectedAddressId] = useState(null);
|
||||||
|
const [addressFormError, setAddressFormError] = useState(null);
|
||||||
|
|
||||||
|
|
||||||
|
const handleAddressSelect = (addressId) => {
|
||||||
|
setSelectedAddressId(addressId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAddress = async (addressId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/user/delete-address/${addressId}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log("Address deleted successfully:", addressId);
|
||||||
|
toast.success("Successfully deleted address", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
// Fetch addresses again after deleting an address
|
||||||
|
fetchAddresses();
|
||||||
|
} else {
|
||||||
|
console.error("Failed to delete address:", response.status);
|
||||||
|
toast.error("Failed to delete address", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting address:", error);
|
||||||
|
toast.error("Error deleting address", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedAddress, setSelectedAddress] = useState({
|
||||||
|
street: "",
|
||||||
|
zipCode: "",
|
||||||
|
city: "",
|
||||||
|
state: "",
|
||||||
|
country: "Philippines",
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const handleContinueAddress = async (e) => {
|
||||||
|
e.preventDefault(); // Prevent the default form submission behavior
|
||||||
|
|
||||||
|
// Add validation logic here to check if the required fields are filled
|
||||||
|
if (
|
||||||
|
!selectedAddress.street ||
|
||||||
|
!selectedAddress.zipCode ||
|
||||||
|
!selectedAddress.city ||
|
||||||
|
!selectedAddress.state
|
||||||
|
) {
|
||||||
|
console.error("Please fill in all required address fields");
|
||||||
|
// Display an error toast
|
||||||
|
toast.error("Please fill in all required address fields", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
// Decode the token to extract user information
|
||||||
|
const decodedToken = jwtDecode(token);
|
||||||
|
const userId = decodedToken.userId;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/user/create-address`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ...selectedAddress, userId }), // Include userId in the request body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Address created successfully:", data);
|
||||||
|
// Optionally, you can update the UI or state to reflect the new address
|
||||||
|
// Display a success toast
|
||||||
|
toast.success("Address created successfully!", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
// Fetch addresses again after creating a new address
|
||||||
|
fetchAddresses();
|
||||||
|
|
||||||
|
// Close the forms after creating a new address
|
||||||
|
setAddAddressFormOpen(false);
|
||||||
|
setPersonalInfoOpen(false);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to create address:", response.status);
|
||||||
|
// Display an error toast
|
||||||
|
toast.error("Failed to create address", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating address:", error);
|
||||||
|
// Display an error toast
|
||||||
|
toast.error("Error creating address", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAddresses = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
// Decode the JWT token to get user information
|
||||||
|
const decoded = jwtDecode(token);
|
||||||
|
const userId = decoded.userId;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/user/get-address`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ userId }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setAddresses(data);
|
||||||
|
|
||||||
|
// Preselect the first address if available
|
||||||
|
if (data.length > 0) {
|
||||||
|
handleAddressSelect(data[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data; // Return the fetched addresses
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch addresses:", response.status);
|
||||||
|
return []; // Return an empty array if fetching fails
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching addresses:", error);
|
||||||
|
return []; // Return an empty array if an error occurs
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchCartItems = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const decoded = jwtDecode(token);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/cart/user/${decoded.userId}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(data.items)) {
|
||||||
|
setCartItems(data.items);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching cart items:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTotalPrice = () => {
|
||||||
|
const totalPriceWithoutShipping = cartItems.reduce((total, item) => {
|
||||||
|
const product = productDetails[item.product];
|
||||||
|
if (product) {
|
||||||
|
return total + product.price * item.quantity;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Calculate the discount based on the percentage
|
||||||
|
const calculatedDiscount =
|
||||||
|
(discountAmount / 100) * totalPriceWithoutShipping;
|
||||||
|
|
||||||
|
// Calculate the total price after applying the discount
|
||||||
|
const totalPriceAfterDiscount =
|
||||||
|
totalPriceWithoutShipping - calculatedDiscount;
|
||||||
|
|
||||||
|
// Use shippingFee.value in the calculation
|
||||||
|
const updatedTotalPrice = totalPriceAfterDiscount + shippingFee.value;
|
||||||
|
|
||||||
|
// Update total price state
|
||||||
|
setTotalPrice(updatedTotalPrice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchProductDetails = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const productIds = cartItems.map((item) => item.product);
|
||||||
|
|
||||||
|
const promises = productIds.map(async (productId) => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/${productId}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(
|
||||||
|
`Failed to fetch product details for ID ${productId}: ${response.status}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productData = await response.json();
|
||||||
|
return { [productId]: productData };
|
||||||
|
});
|
||||||
|
|
||||||
|
const productDetailsArray = await Promise.all(promises);
|
||||||
|
const productDetailsObject = productDetailsArray.reduce(
|
||||||
|
(acc, product) => ({ ...acc, ...product }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
setProductDetails(productDetailsObject);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching product details:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUserProfile = async () => {
|
||||||
|
try {
|
||||||
|
if (isCustomerLoggedIn) {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/user/details`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setUserDetails(data);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch user profile:", response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching user profile:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkCustomerLoginStatus = () => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const decoded = jwtDecode(token);
|
||||||
|
|
||||||
|
if (decoded && decoded.userId) {
|
||||||
|
setIsCustomerLoggedIn(true);
|
||||||
|
fetchCartItems(); // Fetch cart items when the user logs in
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error decoding token:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCustomerLoggedIn(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApplyPromoCode = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/user/apply-promo-code`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
promoCode,
|
||||||
|
discountAmount,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Set discount amount obtained from the response
|
||||||
|
setDiscountAmount(data.discountAmount || 0);
|
||||||
|
|
||||||
|
// Set the applied promo code
|
||||||
|
setAppliedPromoCode(promoCode);
|
||||||
|
|
||||||
|
// Display success notification at the bottom-right
|
||||||
|
toast.success("Promo code applied successfully!", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
checkCustomerLoginStatus();
|
||||||
|
// Wait for state to update before calculating the total price
|
||||||
|
setTimeout(() => {
|
||||||
|
calculateTotalPrice();
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to apply promo code:", response.status);
|
||||||
|
|
||||||
|
// Display error notification at the bottom-right
|
||||||
|
toast.error("Failed to apply promo code", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
// Handle error or provide feedback to the user
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error applying promo code:", error);
|
||||||
|
|
||||||
|
// Display error notification at the bottom-right
|
||||||
|
toast.error("Error applying promo code", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkCustomerLoginStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUserProfile();
|
||||||
|
}, [isCustomerLoggedIn]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCartItems();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProductDetails();
|
||||||
|
}, [cartItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
calculateTotalPrice();
|
||||||
|
}, [cartItems, productDetails, shippingFee]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAddresses();
|
||||||
|
}, [isCustomerLoggedIn]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="my-5 text-start">
|
||||||
|
<h2>Checkout</h2>
|
||||||
|
<Row>
|
||||||
|
{/* Left side - Collapsible Forms */}
|
||||||
|
<Col xs={12} md={8}>
|
||||||
|
{/* Collapsible Table - Personal Information */}
|
||||||
|
<div className="border rounded mb-3 p-3">
|
||||||
|
<h3>
|
||||||
|
<div className="d-flex justify-content-between align-items-start">
|
||||||
|
<span className="fs-6">1. Personal Information</span>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setPersonalInfoOpen(!personalInfoOpen)}
|
||||||
|
aria-controls="personalInfoCollapse"
|
||||||
|
aria-expanded={personalInfoOpen}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={`bi bi-chevron-${
|
||||||
|
personalInfoOpen ? "up" : "down"
|
||||||
|
}`}
|
||||||
|
></i>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<Collapse in={personalInfoOpen}>
|
||||||
|
<div id="personalInfoCollapse">
|
||||||
|
{isCustomerLoggedIn ? (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<strong>Name:</strong> {userDetails?.firstName}{" "}
|
||||||
|
{userDetails?.lastName}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Email:</strong> {userDetails?.email}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Mobile Number:</strong> {userDetails?.mobileNo}
|
||||||
|
</p>
|
||||||
|
<p>If you want to update your information</p>
|
||||||
|
<Button variant="primary" href="/update-profile">
|
||||||
|
Update Information
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mx-4">
|
||||||
|
<p>
|
||||||
|
You are not logged in. Please log in or register to
|
||||||
|
proceed.
|
||||||
|
</p>
|
||||||
|
<Button variant="primary" href="/register">
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
<Button className="mx-2" variant="primary" href="/login">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Collapsible Table - Address */}
|
||||||
|
<div className="border rounded mb-3 p-3">
|
||||||
|
<h3>
|
||||||
|
<div className="d-flex justify-content-between align-items-start">
|
||||||
|
<span className="fs-6">2. Address</span>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setAddressOpen(!addressOpen)}
|
||||||
|
aria-controls="addressCollapse"
|
||||||
|
aria-expanded={addressOpen}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={`bi bi-chevron-${addressOpen ? "up" : "down"}`}
|
||||||
|
></i>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<Collapse in={addressOpen}>
|
||||||
|
<div id="addressCollapse">
|
||||||
|
{isCustomerLoggedIn ? (
|
||||||
|
<Row>
|
||||||
|
<span className="mb-2" style={{ fontSize: "16px" }}>
|
||||||
|
The selected address will be used both as your personal
|
||||||
|
address (for invoice) and as your delivery address.
|
||||||
|
</span>
|
||||||
|
<Col className="border rounded mb-3 p-3" md={6}>
|
||||||
|
{/* Left column - Add New Address Form Dropdown */}
|
||||||
|
<div>
|
||||||
|
{addAddressFormOpen && (
|
||||||
|
<AddressForm
|
||||||
|
selectedAddress={selectedAddress}
|
||||||
|
setSelectedAddress={setSelectedAddress}
|
||||||
|
handleContinueAddress={handleContinueAddress}
|
||||||
|
addressFormError={addressFormError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="my-3"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() =>
|
||||||
|
setAddAddressFormOpen(!addAddressFormOpen)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add New Address
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col md={6}>
|
||||||
|
{/* Right column - My Addresses from addressForm */}
|
||||||
|
{addresses && addresses.length > 0 ? (
|
||||||
|
<Table striped bordered hover>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>My Addresses</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{addresses.map((address, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>
|
||||||
|
<div className="d-flex justify-content-between align-items-center">
|
||||||
|
<Form.Check
|
||||||
|
type="radio"
|
||||||
|
id={`addressRadio${index}`}
|
||||||
|
name="addressRadio"
|
||||||
|
checked={selectedAddressId === address.id}
|
||||||
|
onChange={() =>
|
||||||
|
handleAddressSelect(address.id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="mx-3">
|
||||||
|
{address.street}, {address.city},{" "}
|
||||||
|
{address.state}, {address.zipCode},{" "}
|
||||||
|
{address.country}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="text-danger"
|
||||||
|
onClick={() =>
|
||||||
|
handleDeleteAddress(address._id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="bi bi-trash"></i>
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<p>No existing addresses. Add a new address below.</p>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
) : (
|
||||||
|
<p>
|
||||||
|
User not logged in. Log in to view and manage addresses.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Collapsible Table - Payment */}
|
||||||
|
<div className="border rounded mb-3 p-3">
|
||||||
|
<h3>
|
||||||
|
<div className="d-flex justify-content-between align-items-start">
|
||||||
|
<span className="fs-6">4. Payment</span>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setPaymentOpen(!paymentOpen)}
|
||||||
|
aria-controls="paymentCollapse"
|
||||||
|
aria-expanded={paymentOpen}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={`bi bi-chevron-${paymentOpen ? "up" : "down"}`}
|
||||||
|
></i>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<Collapse in={paymentOpen}>
|
||||||
|
<div id="paymentCollapse">
|
||||||
|
{/* Placeholder content for payment table */}
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
{/* Right side - Order Summary */}
|
||||||
|
<Col xs={12} md={4} className="text-start">
|
||||||
|
<h3>Order Summary</h3>
|
||||||
|
<Table striped bordered hover>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Total Items</td>
|
||||||
|
<td>
|
||||||
|
{cartItems.reduce((total, item) => total + item.quantity, 0)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Shipping Fee</td>
|
||||||
|
<td>{shippingFee.value}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Shipping Details</td>
|
||||||
|
<td>{shippingFee.details}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Total</td>
|
||||||
|
<td>
|
||||||
|
<strong>₱{totalPrice}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Discount</td>
|
||||||
|
<td>
|
||||||
|
<strong>{discountAmount}%</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colSpan="2">
|
||||||
|
<Form.Group controlId="promoCode">
|
||||||
|
<Form.Label>Promo Code</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter promo code"
|
||||||
|
value={promoCode}
|
||||||
|
onChange={(e) => setPromoCode(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="mt-3"
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleApplyPromoCode}
|
||||||
|
>
|
||||||
|
Apply Promo Code
|
||||||
|
</Button>
|
||||||
|
</Form.Group>
|
||||||
|
{appliedPromoCode && (
|
||||||
|
<div>
|
||||||
|
<p className="mt-2">
|
||||||
|
Applied Promo Code: <strong>{appliedPromoCode}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<ToastContainer position="bottom-right" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckoutPage;
|
@ -0,0 +1,16 @@
|
|||||||
|
// NotFound.js
|
||||||
|
import React from 'react';
|
||||||
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const NotFound = () => (
|
||||||
|
<Container className="mt-5">
|
||||||
|
<Row>
|
||||||
|
<Col className="text-center">
|
||||||
|
<h1>404 - Not Found</h1>
|
||||||
|
<p>The page you are looking for does not exist.</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NotFound;
|
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
import Banner from '../components/Banner';
|
||||||
|
import Footer from '../components/Footer';
|
||||||
|
import ProductCatalog from '../components/ProductCatalog';
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
const bannerData = {
|
||||||
|
images: [
|
||||||
|
{ src: '/assets/image1.jpg' },
|
||||||
|
{ src: '/assets/image2.png' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Banner data={bannerData} />
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
<Container className="mt-5">
|
||||||
|
<Row className="d-flex justify-content-center align-items-center">
|
||||||
|
{/* Trusted Couriers */}
|
||||||
|
<Col md={3} className="text-center mb-4">
|
||||||
|
<i className="bi bi-truck text-primary mb-3" style={{ fontSize: '4rem' }}></i>
|
||||||
|
<div className="text-uppercase" style={{ fontSize: '1.4rem' }}>Trusted Couriers</div>
|
||||||
|
<div className='text-opacity-10'>We ship nationwide</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* 7 Days Replacement */}
|
||||||
|
<Col md={3} className="text-center mb-4">
|
||||||
|
<i className="bi bi-arrow-return-left text-primary mb-3" style={{ fontSize: '4rem' }}></i>
|
||||||
|
<div className="text-uppercase" style={{ fontSize: '1.4rem' }}>7 Days Replacement</div>
|
||||||
|
<div>If manufacturer defective</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Best Prices */}
|
||||||
|
<Col md={3} className="text-center mb-4">
|
||||||
|
<i className="bi bi-currency-dollar text-primary mb-3" style={{ fontSize: '4rem' }}></i>
|
||||||
|
<div className="text-uppercase" style={{ fontSize: '1.4rem' }}>Best Prices</div>
|
||||||
|
<div>Huge selections of items</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Trusted Store */}
|
||||||
|
<Col md={3} className="text-center mb-4">
|
||||||
|
<i className="bi bi-check-circle text-primary mb-3" style={{ fontSize: '4rem' }}></i>
|
||||||
|
<div className="text-uppercase" style={{ fontSize: '1.4rem' }}>Trusted Store</div>
|
||||||
|
<div>Tons of positive feedbacks</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<ProductCatalog />
|
||||||
|
</Container>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
@ -0,0 +1,160 @@
|
|||||||
|
// Login.js
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
|
import { Form, Button, Alert, Container, Col } from 'react-bootstrap';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
|
import UserContext from '../UserContext';
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const { user, setUser } = useContext(UserContext);
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
const [loginError, setLoginError] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const authenticate = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/user/authenticate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.token) {
|
||||||
|
localStorage.setItem('token', data.token);
|
||||||
|
retrieveUserDetails(data.token);
|
||||||
|
|
||||||
|
setLoginError(null);
|
||||||
|
setIsActive(false);
|
||||||
|
|
||||||
|
toast.success('Login successful!', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.isAdmin) {
|
||||||
|
navigate('/dashboard');
|
||||||
|
} else {
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLoginError('Authentication failed. Check your login details and try again.');
|
||||||
|
|
||||||
|
toast.error('Authentication failed. Check your login details and try again.', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during login:', error);
|
||||||
|
|
||||||
|
setLoginError('An unexpected error occurred. Please try again later.');
|
||||||
|
|
||||||
|
toast.error('An unexpected error occurred. Please try again later.', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setEmail('');
|
||||||
|
setPassword('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const retrieveUserDetails = async (token) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/user/details`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setUser({
|
||||||
|
id: data._id,
|
||||||
|
isAdmin: data.isAdmin,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Error retrieving user details:', data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during user details retrieval:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsActive(email !== '' && password !== '');
|
||||||
|
}, [email, password]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
user.id !== null ? (
|
||||||
|
<Navigate to="/" />
|
||||||
|
) : (
|
||||||
|
<Container className="my-5">
|
||||||
|
<Col md={{ span: 6, offset: 3 }}>
|
||||||
|
<Form onSubmit={(e) => authenticate(e)} className="shadow p-5 mb-5 bg-white rounded">
|
||||||
|
<h1 className="text-center mb-4">Login</h1>
|
||||||
|
|
||||||
|
{loginError && <Alert variant="danger">{loginError}</Alert>}
|
||||||
|
|
||||||
|
<Form.Group controlId="userEmail">
|
||||||
|
<Form.Label>Email address</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="password">
|
||||||
|
<Form.Label>Password</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{isActive ? (
|
||||||
|
<Button variant="primary" type="submit" id="submitBtn">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="danger" type="submit" id="submitBtn" disabled>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { useContext, useEffect } from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import UserContext from '../UserContext';
|
||||||
|
import { useCart } from '../CartContext';
|
||||||
|
|
||||||
|
export default function Logout() {
|
||||||
|
const { unsetUser, setUser } = useContext(UserContext);
|
||||||
|
const { clearCart } = useCart();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
// Call the clearCart function from the CartContext
|
||||||
|
await clearCart();
|
||||||
|
// Display success toast for cart clearing, using toastId to ensure it only notifies once
|
||||||
|
toast.success('Your cart is cleared.', { position: 'bottom-right', toastId: 'cart-cleared-toast' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing cart:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset user locally
|
||||||
|
unsetUser();
|
||||||
|
setUser({
|
||||||
|
id: null,
|
||||||
|
isAdmin: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect back to login
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLogout();
|
||||||
|
}, [unsetUser, setUser, clearCart]);
|
||||||
|
|
||||||
|
return <Navigate to="/login" />;
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Container, Row, Col, Image, Button } from "react-bootstrap";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
import { BsDash, BsPlus } from "react-icons/bs";
|
||||||
|
import "./productPage.css";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
|
const ProductPage = () => {
|
||||||
|
const { productId } = useParams();
|
||||||
|
const [product, setProduct] = useState(null);
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProduct = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/product/${productId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch product");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setProduct(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching product by ID:", error);
|
||||||
|
setError("Error fetching product. Please try again.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchProduct();
|
||||||
|
}, [productId]);
|
||||||
|
|
||||||
|
const handleAddToCart = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
let anonymousUserId = localStorage.getItem("anonymousUserId");
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
let userId; // variable to hold either userId or anonymousUserId
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = `Bearer ${token}`;
|
||||||
|
userId = jwtDecode(token).userId;
|
||||||
|
} else if (!anonymousUserId) {
|
||||||
|
// Generate a new anonymousUserId if it doesn't exist
|
||||||
|
anonymousUserId = uuidv4();
|
||||||
|
localStorage.setItem("anonymousUserId", anonymousUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers["X-Anonymous-User-ID"] = anonymousUserId;
|
||||||
|
userId = null; // Set userId to null if not logged in
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/cart/add-to-cart`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId,
|
||||||
|
anonymousUserId,
|
||||||
|
productId,
|
||||||
|
quantity,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(data.message, {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 2000,
|
||||||
|
hideProgressBar: true,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(data.error || "Error adding to cart", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 2000,
|
||||||
|
hideProgressBar: true,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding to cart:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuantityDecrease = () => {
|
||||||
|
if (quantity > 1) {
|
||||||
|
setQuantity(quantity - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuantityIncrease = () => {
|
||||||
|
setQuantity(quantity + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<h2>Error</h2>
|
||||||
|
<p>{error}</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<p>Loading...</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="mt-4">
|
||||||
|
<Row>
|
||||||
|
<Col md={6} className="d-flex justify-content-center">
|
||||||
|
<Image
|
||||||
|
src={product.image || "placeholder-image-url"}
|
||||||
|
alt={product.name}
|
||||||
|
fluid
|
||||||
|
className="product-image"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col md={6} className="product-details">
|
||||||
|
<h2 className="product-title">{product.name}</h2>
|
||||||
|
<p className="product-id">ID: {product._id}</p>
|
||||||
|
<div className="product-description">
|
||||||
|
<p>
|
||||||
|
<strong>Description:</strong>
|
||||||
|
</p>
|
||||||
|
<p>{product.description}</p>
|
||||||
|
{/* Add more bullet points as needed */}
|
||||||
|
</div>
|
||||||
|
<p className="product-price">
|
||||||
|
<p>
|
||||||
|
<strong>Price:</strong>
|
||||||
|
</p>
|
||||||
|
PHP{" "}
|
||||||
|
{new Intl.NumberFormat("en-PH", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "PHP",
|
||||||
|
}).format(product.price)}
|
||||||
|
</p>
|
||||||
|
<div className="quantity-selector">
|
||||||
|
<Button variant="outline-primary" onClick={handleQuantityDecrease}>
|
||||||
|
<BsDash className="rounded" />
|
||||||
|
</Button>
|
||||||
|
<span className="quantity-value">{quantity}</span>
|
||||||
|
<Button variant="outline-primary" onClick={handleQuantityIncrease}>
|
||||||
|
<BsPlus />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="button-group mt-3">
|
||||||
|
<Button variant="primary" onClick={handleAddToCart}>
|
||||||
|
Add to Cart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductPage;
|
@ -0,0 +1,87 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Row, Col, Button, Container } from 'react-bootstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import UserContext from '../UserContext';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function Profile({ setUserDetails }) {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const [profileData, setProfileData] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserProfile = async () => {
|
||||||
|
try {
|
||||||
|
if (user && user.id) {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.REACT_APP_API_URL}/user/details`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setProfileData(data);
|
||||||
|
|
||||||
|
// Pass user details to the parent component
|
||||||
|
setUserDetails(data);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch user profile:', response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user profile:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserProfile();
|
||||||
|
}, [user, setUserDetails]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<Col md={6} className="shadow-sm p-5 bg-white">
|
||||||
|
<h1 className="my-5">Profile</h1>
|
||||||
|
{loading ? (
|
||||||
|
<p>Loading profile...</p>
|
||||||
|
) : profileData ? (
|
||||||
|
<>
|
||||||
|
<h2 className="mt-3">
|
||||||
|
{profileData.firstName} {profileData.lastName}
|
||||||
|
</h2>
|
||||||
|
<hr />
|
||||||
|
<p>Email: {profileData.email}</p>
|
||||||
|
<p>Mobile Number: {profileData.mobileNo}</p>
|
||||||
|
<p>Account Type: {profileData.isAdmin ? 'Admin' : 'User'}</p>
|
||||||
|
<h3>Orders</h3>
|
||||||
|
{profileData.orderedProducts.length > 0 ? (
|
||||||
|
<ul>
|
||||||
|
{profileData.orderedProducts.map((order, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
Order {index + 1}: {order.totalAmount}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>No orders yet.</p>
|
||||||
|
)}
|
||||||
|
<Link to="/update-profile">
|
||||||
|
<Button variant="primary">Update Profile</Button>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>No profile data available.</p>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
// Register.js
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Container, Col } from 'react-bootstrap';
|
||||||
|
import RegisterForm from '../components/Register';
|
||||||
|
|
||||||
|
const Register = () => {
|
||||||
|
return (
|
||||||
|
<Container className="my-5">
|
||||||
|
<Col lg={{ span: 6, offset: 3 }}>
|
||||||
|
{/* Your other content */}
|
||||||
|
<RegisterForm />
|
||||||
|
{/* Your other content */}
|
||||||
|
</Col>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Register;
|
@ -0,0 +1,16 @@
|
|||||||
|
// TokenExpired.js
|
||||||
|
import React from 'react';
|
||||||
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const TokenExpired = () => (
|
||||||
|
<Container className="mt-5">
|
||||||
|
<Row>
|
||||||
|
<Col className="text-center">
|
||||||
|
<h1>Token Expired</h1>
|
||||||
|
<p>Your session has expired. Please log in again.</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TokenExpired;
|
@ -0,0 +1,79 @@
|
|||||||
|
/* ProductPage.css */
|
||||||
|
.product-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
max-width: 80%; /* Adjust image size */
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-id,
|
||||||
|
.product-price {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-description {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-description strong {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-description ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price {
|
||||||
|
text-decoration: none; /* Remove the line in the middle */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quantity Controls Styling */
|
||||||
|
.input-group-prepend,
|
||||||
|
.input-group-append {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-prepend button,
|
||||||
|
.input-group-append button {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* productPage.css */
|
||||||
|
|
||||||
|
.quantity-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-selector button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-value {
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
|||||||
|
// cartActions.js
|
||||||
|
|
||||||
|
export const addToCart = (productId, quantity) => {
|
||||||
|
return {
|
||||||
|
type: 'ADD_TO_CART',
|
||||||
|
payload: { productId, quantity },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,34 @@
|
|||||||
|
// src/redux/cartReducer.js
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
cartItems: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const cartReducer = (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_TO_CART':
|
||||||
|
const { productId } = action.payload;
|
||||||
|
const existingItem = state.cartItems.find((item) => item.productId === productId);
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cartItems: state.cartItems.map((item) =>
|
||||||
|
item.productId === productId ? { ...item, quantity: item.quantity + 1 } : item
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cartItems: [...state.cartItems, { productId, quantity: 1 }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add more cases for other actions if needed
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default cartReducer;
|
@ -0,0 +1,8 @@
|
|||||||
|
// src/redux/store.js
|
||||||
|
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
import cartReducer from './cartReducer';
|
||||||
|
|
||||||
|
const store = createStore(cartReducer);
|
||||||
|
|
||||||
|
export default store;
|