diff --git a/BACKEND/controllers/product.controller.js b/BACKEND/controllers/product.controller.js index 187cb28..a242ae1 100644 --- a/BACKEND/controllers/product.controller.js +++ b/BACKEND/controllers/product.controller.js @@ -1,4 +1,4 @@ -import Product from "../models/product.model.js"; +import Product from '../models/product.model.js'; import mongoose from "mongoose"; import { escapeRegex } from '../utils/escapeRegex.js'; import cloudinary from '../config/cloudinary.js'; @@ -202,6 +202,11 @@ export const createProduct = async (req, res, next) => { export const updateProduct = async (req, res, next) => { const { id } = req.params; + if (!product.name || !product.price || !product.image) { + return res.status(400).json({ success: false, message: "Please provide all fields" }); + } + + // ─── VALIDATE TAGS ───────────────────────────────────────────── if (!mongoose.Types.ObjectId.isValid(id)) { return next(new AppError("Invalid Product Id format", 404)); } @@ -210,6 +215,11 @@ export const updateProduct = async (req, res, next) => { return next(new AppError("No update fields provided", 400)); } + const newProduct = new Product(product); + + try { + await newProduct.save(); + res.status(201).json({ success: true, data: newProduct }); let existing; try { existing = await Product.findById(id); @@ -317,11 +327,17 @@ export const deleteProduct = async (req, res, next) => { await invalidateProductCache(); res.status(200).json({ success: true, message: "Product deleted successfully" }); } catch (error) { - console.log("error in deleting product:", error.message); + console.error("Error in Create product:", error.message); res.status(500).json({ success: false, message: "Server Error" }); } }; +export const updateProduct = async (req, res) => { + const { id } = req.params; + const product = req.body; +// ─── SEARCH PRODUCTS (INCLUDING TAGS) ──────────────────────────── +export const searchProducts = async (req, res) => { + const { q } = req.query; // @desc Get product by ID export const getProductById = async (req, res, next) => { const { id } = req.params; @@ -330,6 +346,27 @@ export const getProductById = async (req, res, next) => { return next(new AppError("Invalid Product Id format", 404)); } + // ─── VALIDATE TAGS ON UPDATE ──────────────────────────────────── + if (product.tags && product.tags.length > 5) { + return res.status(400).json({ + success: false, + message: "Maximum 5 tags allowed per product" + }); + } + + try { + const updatedProduct = await Product.findByIdAndUpdate(id, product, { + new: true, + runValidators: true + }); + + if (!updatedProduct) { + return res.status(404).json({ success: false, message: "Product not found" }); + } + + res.status(200).json({ success: true, data: updatedProduct }); + } catch (error) { + console.error("Update error:", error); try { const product = await Product.findOne({ _id: id, isDeleted: { $ne: true } }); if (!product) { @@ -423,6 +460,7 @@ export const getRelatedProducts = async (req, res) => { } }; +export const deleteProduct = async (req, res) => { export const getProductBundle = async (req, res) => { const { id } = req.params; @@ -431,6 +469,10 @@ export const getProductBundle = async (req, res) => { } try { + await Product.findByIdAndDelete(id); + res.status(200).json({ success: true, message: "Product deleted" }); + } catch (error) { + console.log("error in deleting product:", error.message); const product = await Product.findById(id).populate('complementaryItems.product'); if (!product || product.isDeleted === true) { return res.status(404).json({ success: false, message: "Product not found" }); @@ -467,6 +509,25 @@ export const getProductBundle = async (req, res) => { } }; +// ─── SEARCH PRODUCTS (INCLUDING TAGS) ──────────────────────────── +export const searchProducts = async (req, res) => { + const { q } = req.query; + + if (!q) { + return res.status(400).json({ success: false, message: "Search query required" }); + } + + try { + const regex = new RegExp(q, 'i'); + + const products = await Product.find({ + $or: [ + { name: regex }, + { tags: { $in: [regex] } } + ] + }); + + res.status(200).json({ success: true, data: products }); // @desc Search products export const searchProducts = async (req, res, next) => { const { q } = req.query; @@ -491,3 +552,4 @@ export const searchProducts = async (req, res, next) => { next(error); } }; +}; diff --git a/BACKEND/models/product.model.js b/BACKEND/models/product.model.js index a34b374..28dcb48 100644 --- a/BACKEND/models/product.model.js +++ b/BACKEND/models/product.model.js @@ -32,7 +32,25 @@ const productSchema = new mongoose.Schema({ type: [String], default: [], }, - + + // ─── TAGS ────────────────────────────────────────────────────── + tags: { + type: [String], + default: [], + validate: { + validator: function (tags) { + return tags.length <= 5 && tags.every(tag => tag.length >= 2 && tag.length <= 30); + }, + message: "Maximum 5 tags, each 2-30 characters" + } + } +}, { + timestamps: true +}); + +const Product = mongoose.model("Product", productSchema); + +export default Product; // Optional Fields - Extra Product Details description: { type: String, diff --git a/FRONTEND/src/components/ui/ProductCard.jsx b/FRONTEND/src/components/ui/ProductCard.jsx index 3000265..09a44cd 100644 --- a/FRONTEND/src/components/ui/ProductCard.jsx +++ b/FRONTEND/src/components/ui/ProductCard.jsx @@ -25,21 +25,21 @@ import { useColorModeValue, useDisclosure, useToast, - VStack, -} from "@chakra-ui/react"; -import React, { useEffect, useRef, useState } from "react"; -import { FaBalanceScale, FaEdit, FaHeart, FaRegHeart, FaTrash } from "react-icons/fa"; + VStack +} from '@chakra-ui/react'; +import React, { useEffect, useRef, useState } from 'react'; import { Link } from "react-router-dom"; +import { FaEdit, FaTrash, FaHeart, FaRegHeart, FaBalanceScale } from "react-icons/fa"; +import { useProductStore } from "../../store/product"; import { useCart } from "../../store/cart"; import { useCurrencyStore } from "../../store/currency"; -import { useProductStore } from "../../store/product"; -import { useWishlist } from "../../context/WishlistContext.jsx"; import { formatPrice } from "../../utils/currency"; +import { useWishlist } from "../../context/WishlistContext.jsx"; import { - showErrorToast, - showInfoToast, showSuccessToast, + showErrorToast, showWarningToast, + showInfoToast, } from "../../utils/toastHelpers"; const ProductCard = ({ product }) => { @@ -61,6 +61,8 @@ const ProductCard = ({ product }) => { const borderColor = useColorModeValue("gray.200", "gray.700"); const optionalLabelColor = useColorModeValue("gray.600", "gray.300"); + // ✅ SINGLE declaration - NO duplicates + const { deleteProduct, updateProduct, addToCompare, compareList = [], isSubmitting, isDeleting } = useProductStore(); const { deleteProduct, updateProduct, @@ -78,23 +80,17 @@ const ProductCard = ({ product }) => { const toast = useToast(); const { isOpen, onOpen, onClose } = useDisclosure(); - const { - isOpen: isDeleteOpen, - onOpen: onDeleteOpen, - onClose: onDeleteClose, - } = useDisclosure(); + const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure(); const LOW_STOCK_THRESHOLD = 5; const isOutOfStock = product.stock != null && product.stock === 0; const isLowStock = product.stock != null && product.stock > 0 && product.stock <= LOW_STOCK_THRESHOLD; - // Sync updatedProduct when product prop changes useEffect(() => { setUpdatedProduct(product); setImagePreview(product.image); }, [product]); - // Check wishlist status on mount useEffect(() => { const checkWishlist = async () => { const inWishlist = await checkInWishlist(product._id); @@ -103,7 +99,6 @@ const ProductCard = ({ product }) => { checkWishlist(); }, [product._id, checkInWishlist]); - // Revoke blob URLs to avoid memory leaks useEffect(() => { const url = imagePreview; return () => { @@ -207,6 +202,18 @@ const ProductCard = ({ product }) => { }} bg={bg} > + + {product.name} + { - {/* Product Name with Link */} - - + + {product.name} - {/* Price */} - + {formatPrice(product.price, currency, rates)} - {/* Tags */} + {/* ─── TAGS DISPLAY ────────────────────────────────────────── */} {product.tags && product.tags.length > 0 && ( {product.tags.map((tag, index) => ( @@ -281,7 +286,10 @@ const ProductCard = ({ product }) => { borderRadius="full" bg="blue.100" color="blue.800" - _dark={{ bg: "blue.900", color: "blue.200" }} + _dark={{ + bg: "blue.900", + color: "blue.200" + }} > #{tag} @@ -292,7 +300,6 @@ const ProductCard = ({ product }) => { {/* Action Buttons */} - {/* Wishlist */} : } onClick={handleWishlistToggle} @@ -304,7 +311,6 @@ const ProductCard = ({ product }) => { _hover={{ transform: "scale(1.1)" }} /> - {/* Edit */} } onClick={handleModalOpen} @@ -315,7 +321,6 @@ const ProductCard = ({ product }) => { _hover={{ transform: "scale(1.1)" }} /> - {/* Delete */} } onClick={onDeleteOpen} @@ -326,7 +331,6 @@ const ProductCard = ({ product }) => { _hover={{ transform: "scale(1.1)" }} /> - {/* Compare */} } onClick={() => addToCompare(product)} @@ -346,7 +350,6 @@ const ProductCard = ({ product }) => { /> - {/* Add to Cart */}