diff --git a/.env b/.env index aa617f82..235613cd 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ PORT=5000 -MONGO_URI='mongodb://127.0.0.1:27017/huxnStore' +MONGO_URI='mongodb+srv://sam:sam321123@cluster0.6psiz.mongodb.net/eStore' +JWT_SECRET=abaya2025 NODE_ENV=development -JWT_SECRET=abac12afsdkjladf -PAYPAL_CLIENT_ID= \ No newline at end of file + diff --git a/backend/config/multer.js b/backend/config/multer.js new file mode 100644 index 00000000..6b97874f --- /dev/null +++ b/backend/config/multer.js @@ -0,0 +1,52 @@ +import multer from 'multer'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Ensure uploads directory exists +const uploadsDir = path.join(__dirname, '../uploads'); +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} + +// Configure storage +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + console.log('Destination directory:', uploadsDir); + cb(null, uploadsDir); + }, + filename: function (req, file, cb) { + // Create unique filename with timestamp + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const filename = file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname); + console.log('Generated filename:', filename); + cb(null, filename); + } +}); + +// File filter to accept only images +const fileFilter = (req, file, cb) => { + console.log('File being processed:', file); + const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp']; + if (allowedTypes.includes(file.mimetype)) { + console.log('File accepted:', file.originalname); + cb(null, true); + } else { + console.log('File rejected:', file.originalname, 'MIME type:', file.mimetype); + cb(new Error('Invalid file type. Only JPEG, PNG, JPG, and WEBP are allowed.'), false); + } +}; + +// Configure multer +const upload = multer({ + storage: storage, + fileFilter: fileFilter, + limits: { + fileSize: 5 * 1024 * 1024 // 5MB limit + } +}); + +export { upload }; \ No newline at end of file diff --git a/backend/controllers/productController.js b/backend/controllers/productController.js index 19e69389..736b3045 100644 --- a/backend/controllers/productController.js +++ b/backend/controllers/productController.js @@ -1,137 +1,156 @@ import asyncHandler from "../middlewares/asyncHandler.js"; import Product from "../models/productModel.js"; +import { authenticate, authorizeAdmin } from '../middlewares/authMiddleware.js'; +import { upload } from '../config/multer.js'; +import path from 'path'; -const addProduct = asyncHandler(async (req, res) => { +// Create a new product +const createProduct = asyncHandler(async (req, res) => { try { - const { name, description, price, category, quantity, brand } = req.fields; - + const { name, description, price, category, countInStock, brand } = req.body; + // Validation switch (true) { case !name: - return res.json({ error: "Name is required" }); + return res.status(400).json({ error: "Name is required" }); case !brand: - return res.json({ error: "Brand is required" }); + return res.status(400).json({ error: "Brand is required" }); case !description: - return res.json({ error: "Description is required" }); + return res.status(400).json({ error: "Description is required" }); case !price: - return res.json({ error: "Price is required" }); + return res.status(400).json({ error: "Price is required" }); case !category: - return res.json({ error: "Category is required" }); - case !quantity: - return res.json({ error: "Quantity is required" }); + return res.status(400).json({ error: "Category is required" }); + case !countInStock: + return res.status(400).json({ error: "Count in stock is required" }); + case !req.file: + return res.status(400).json({ error: "Image is required" }); } - const product = new Product({ ...req.fields }); - await product.save(); - res.json(product); + // Store relative path for the image + const imagePath = path.relative(path.join(process.cwd(), 'backend'), req.file.path); + + const product = new Product({ + name, + description, + price, + category, + countInStock, + brand, + image: imagePath + }); + + const createdProduct = await product.save(); + res.status(201).json(createdProduct); } catch (error) { - console.error(error); - res.status(400).json(error.message); + console.error('Error creating product:', error); + res.status(500).json({ error: 'Failed to create product', details: error.message }); } }); -const updateProductDetails = asyncHandler(async (req, res) => { +// Update a product +const updateProduct = asyncHandler(async (req, res) => { try { - const { name, description, price, category, quantity, brand } = req.fields; - - // Validation - switch (true) { - case !name: - return res.json({ error: "Name is required" }); - case !brand: - return res.json({ error: "Brand is required" }); - case !description: - return res.json({ error: "Description is required" }); - case !price: - return res.json({ error: "Price is required" }); - case !category: - return res.json({ error: "Category is required" }); - case !quantity: - return res.json({ error: "Quantity is required" }); + const { name, description, price, category, countInStock, brand } = req.body; + + const product = await Product.findById(req.params.id); + + if (!product) { + return res.status(404).json({ message: 'Product not found' }); } - const product = await Product.findByIdAndUpdate( - req.params.id, - { ...req.fields }, - { new: true } - ); - - await product.save(); + product.name = name || product.name; + product.description = description || product.description; + product.price = price || product.price; + product.category = category || product.category; + product.countInStock = countInStock || product.countInStock; + product.brand = brand || product.brand; + + if (req.file) { + product.image = path.relative(path.join(process.cwd(), 'backend'), req.file.path); + } - res.json(product); + const updatedProduct = await product.save(); + res.json(updatedProduct); } catch (error) { - console.error(error); - res.status(400).json(error.message); + console.error('Error updating product:', error); + res.status(500).json({ error: 'Failed to update product', details: error.message }); } }); +// Delete a product const removeProduct = asyncHandler(async (req, res) => { try { const product = await Product.findByIdAndDelete(req.params.id); - res.json(product); + if (!product) { + return res.status(404).json({ message: 'Product not found' }); + } + res.json({ message: 'Product removed' }); } catch (error) { - console.error(error); - res.status(500).json({ error: "Server error" }); + console.error('Error removing product:', error); + res.status(500).json({ error: 'Failed to remove product', details: error.message }); } }); +// Get all products const fetchProducts = asyncHandler(async (req, res) => { try { const pageSize = 6; + const page = Number(req.query.pageNumber) || 1; const keyword = req.query.keyword ? { name: { $regex: req.query.keyword, - $options: "i", + $options: 'i', }, } : {}; const count = await Product.countDocuments({ ...keyword }); - const products = await Product.find({ ...keyword }).limit(pageSize); + const products = await Product.find({ ...keyword }) + .limit(pageSize) + .skip(pageSize * (page - 1)); res.json({ products, - page: 1, + page, pages: Math.ceil(count / pageSize), - hasMore: false, }); } catch (error) { - console.error(error); - res.status(500).json({ error: "Server Error" }); + console.error('Error fetching products:', error); + res.status(500).json({ error: 'Failed to fetch products', details: error.message }); } }); +// Get product by ID const fetchProductById = asyncHandler(async (req, res) => { try { const product = await Product.findById(req.params.id); if (product) { - return res.json(product); + res.json(product); } else { res.status(404); - throw new Error("Product not found"); + throw new Error('Product not found'); } } catch (error) { - console.error(error); - res.status(404).json({ error: "Product not found" }); + console.error('Error fetching product:', error); + res.status(500).json({ error: 'Failed to fetch product', details: error.message }); } }); +// Get all products (admin) const fetchAllProducts = asyncHandler(async (req, res) => { try { - const products = await Product.find({}) - .populate("category") - .limit(12) - .sort({ createAt: -1 }); - + const products = await Product.find({}); res.json(products); } catch (error) { - console.error(error); - res.status(500).json({ error: "Server Error" }); + console.error('Error fetching all products:', error); + res.status(500).json({ error: 'Failed to fetch all products', details: error.message }); } }); +// Add product review const addProductReview = asyncHandler(async (req, res) => { try { const { rating, comment } = req.body; @@ -144,75 +163,81 @@ const addProductReview = asyncHandler(async (req, res) => { if (alreadyReviewed) { res.status(400); - throw new Error("Product already reviewed"); + throw new Error('Product already reviewed'); } const review = { - name: req.user.username, + name: req.user.name, rating: Number(rating), comment, user: req.user._id, }; product.reviews.push(review); - product.numReviews = product.reviews.length; - product.rating = product.reviews.reduce((acc, item) => item.rating + acc, 0) / product.reviews.length; await product.save(); - res.status(201).json({ message: "Review added" }); + res.status(201).json({ message: 'Review added' }); } else { res.status(404); - throw new Error("Product not found"); + throw new Error('Product not found'); } } catch (error) { - console.error(error); - res.status(400).json(error.message); + console.error('Error adding review:', error); + res.status(500).json({ error: 'Failed to add review', details: error.message }); } }); +// Get top rated products const fetchTopProducts = asyncHandler(async (req, res) => { try { - const products = await Product.find({}).sort({ rating: -1 }).limit(4); + const products = await Product.find({}).sort({ rating: -1 }).limit(3); res.json(products); } catch (error) { - console.error(error); - res.status(400).json(error.message); + console.error('Error fetching top products:', error); + res.status(500).json({ error: 'Failed to fetch top products', details: error.message }); } }); +// Get new products const fetchNewProducts = asyncHandler(async (req, res) => { try { - const products = await Product.find().sort({ _id: -1 }).limit(5); + const products = await Product.find().sort({ createdAt: -1 }).limit(5); res.json(products); } catch (error) { - console.error(error); - res.status(400).json(error.message); + console.error('Error fetching new products:', error); + res.status(500).json({ error: 'Failed to fetch new products', details: error.message }); } }); +// Filter products const filterProducts = asyncHandler(async (req, res) => { try { - const { checked, radio } = req.body; - - let args = {}; - if (checked.length > 0) args.category = checked; - if (radio.length) args.price = { $gte: radio[0], $lte: radio[1] }; + const { category, minPrice, maxPrice, rating } = req.body; + let query = {}; + + if (category) query.category = category; + if (minPrice || maxPrice) { + query.price = {}; + if (minPrice) query.price.$gte = minPrice; + if (maxPrice) query.price.$lte = maxPrice; + } + if (rating) query.rating = { $gte: rating }; - const products = await Product.find(args); + const products = await Product.find(query); res.json(products); } catch (error) { - console.error(error); - res.status(500).json({ error: "Server Error" }); + console.error('Error filtering products:', error); + res.status(500).json({ error: 'Failed to filter products', details: error.message }); } }); export { - addProduct, - updateProductDetails, + createProduct, + updateProduct, removeProduct, fetchProducts, fetchProductById, @@ -220,5 +245,5 @@ export { addProductReview, fetchTopProducts, fetchNewProducts, - filterProducts, + filterProducts }; diff --git a/backend/index.js b/backend/index.js index 0f87fbe9..0dbf2a6c 100644 --- a/backend/index.js +++ b/backend/index.js @@ -3,6 +3,9 @@ import path from "path"; import express from "express"; import dotenv from "dotenv"; import cookieParser from "cookie-parser"; +import { notFound, errorHandler } from './middlewares/errorMiddleware.js'; +import { fileURLToPath } from 'url'; +import cors from 'cors'; // Utiles import connectDB from "./config/db.js"; @@ -22,6 +25,7 @@ const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); +app.use(cors()); app.use("/api/users", userRoutes); app.use("/api/category", categoryRoutes); @@ -33,7 +37,17 @@ app.get("/api/config/paypal", (req, res) => { res.send({ clientId: process.env.PAYPAL_CLIENT_ID }); }); -const __dirname = path.resolve(); -app.use("/uploads", express.static(path.join(__dirname + "/uploads"))); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); -app.listen(port, () => console.log(`Server running on port: ${port}`)); +// Serve static files from the uploads directory +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + +// Error handling +app.use(notFound); +app.use(errorHandler); + +app.listen(port, () => { + console.log(`Server running on port: ${port}`); + console.log(`Uploads directory: ${path.join(__dirname, 'uploads')}`); +}); diff --git a/backend/middlewares/errorMiddleware.js b/backend/middlewares/errorMiddleware.js new file mode 100644 index 00000000..f263a4db --- /dev/null +++ b/backend/middlewares/errorMiddleware.js @@ -0,0 +1,20 @@ +import asyncHandler from './asyncHandler.js'; + +// Not found middleware +const notFound = (req, res, next) => { + const error = new Error(`Not Found - ${req.originalUrl}`); + res.status(404); + next(error); +}; + +// Error handling middleware +const errorHandler = (err, req, res, next) => { + const statusCode = res.statusCode === 200 ? 500 : res.statusCode; + res.status(statusCode); + res.json({ + message: err.message, + stack: process.env.NODE_ENV === 'production' ? null : err.stack, + }); +}; + +export { notFound, errorHandler }; \ No newline at end of file diff --git a/backend/models/productModel.js b/backend/models/productModel.js index 3c72b426..b5af64e8 100644 --- a/backend/models/productModel.js +++ b/backend/models/productModel.js @@ -18,16 +18,19 @@ const reviewSchema = mongoose.Schema( const productSchema = mongoose.Schema( { name: { type: String, required: true }, + slug: { type: String, required: true, unique: true }, + category: { type: String, required: true }, image: { type: String, required: true }, + images: [String], + price: { type: Number, required: true }, brand: { type: String, required: true }, - quantity: { type: Number, required: true }, - category: { type: ObjectId, ref: "Category", required: true }, - description: { type: String, required: true }, - reviews: [reviewSchema], rating: { type: Number, required: true, default: 0 }, numReviews: { type: Number, required: true, default: 0 }, - price: { type: Number, required: true, default: 0 }, countInStock: { type: Number, required: true, default: 0 }, + description: { type: String, required: true }, + isFeatured: { type: Boolean, default: false }, + banner: String, + reviews: [reviewSchema], }, { timestamps: true } ); diff --git a/backend/routes/productRoutes.js b/backend/routes/productRoutes.js index ab94c052..a96d6dc7 100644 --- a/backend/routes/productRoutes.js +++ b/backend/routes/productRoutes.js @@ -1,40 +1,54 @@ import express from "express"; -import formidable from "express-formidable"; -const router = express.Router(); - -// controllers -import { - addProduct, - updateProductDetails, +import { + createProduct, + fetchProducts, + fetchProductById, + updateProduct, removeProduct, - fetchProducts, - fetchProductById, fetchAllProducts, addProductReview, fetchTopProducts, fetchNewProducts, - filterProducts, -} from "../controllers/productController.js"; -import { authenticate, authorizeAdmin } from "../middlewares/authMiddleware.js"; + filterProducts +} from '../controllers/productController.js'; +import { upload } from '../config/multer.js'; +import { authenticate, authorizeAdmin } from '../middlewares/authMiddleware.js'; import checkId from "../middlewares/checkId.js"; +import Product from "../models/productModel.js"; -router - .route("/") - .get(fetchProducts) - .post(authenticate, authorizeAdmin, formidable(), addProduct); - -router.route("/allproducts").get(fetchAllProducts); -router.route("/:id/reviews").post(authenticate, checkId, addProductReview); +const router = express.Router(); +// Public routes +router.get("/", fetchProducts); +router.get("/all", fetchAllProducts); router.get("/top", fetchTopProducts); router.get("/new", fetchNewProducts); +router.get("/:id", fetchProductById); +router.post("/filter", filterProducts); + +// Protected routes +router.post("/:id/reviews", authenticate, checkId, addProductReview); -router - .route("/:id") - .get(fetchProductById) - .put(authenticate, authorizeAdmin, formidable(), updateProductDetails) - .delete(authenticate, authorizeAdmin, removeProduct); +// Admin routes +router.post("/", authenticate, authorizeAdmin, upload.single('image'), createProduct); +router.put("/:id", authenticate, authorizeAdmin, upload.single('image'), updateProduct); +router.delete("/:id", authenticate, authorizeAdmin, removeProduct); -router.route("/filtered-products").post(filterProducts); +// Route for uploading multiple images +router.post('/:id/images', authenticate, authorizeAdmin, upload.array('images', 5), async (req, res) => { + try { + const product = await Product.findById(req.params.id); + if (product) { + const newImages = req.files.map(file => file.path); + product.images = [...product.images, ...newImages]; + await product.save(); + res.json(product); + } else { + res.status(404).json({ message: 'Product not found' }); + } + } catch (error) { + res.status(400).json({ message: error.message }); + } +}); export default router; diff --git a/frontend/package.json b/frontend/package.json index c2b59cc3..8d9f169f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "frontend", + "name": "abaya-closet-frontend", "private": true, "version": "0.0.0", "type": "module", diff --git a/frontend/src/components/Message.jsx b/frontend/src/components/Message.jsx index 8fc94669..526d0294 100644 --- a/frontend/src/components/Message.jsx +++ b/frontend/src/components/Message.jsx @@ -1,16 +1,23 @@ -const Message = ({ variant, children }) => { +const Message = ({ variant = 'info', children }) => { const getVariantClass = () => { switch (variant) { - case "succcess": + case "success": return "bg-green-100 text-green-800"; + case "danger": case "error": return "bg-red-100 text-red-800"; + case "warning": + return "bg-yellow-100 text-yellow-800"; default: return "bg-blue-100 text-blue-800"; } }; - return