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 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 */}
- {/* ── Delete Confirmation Dialog ── */}
+ {/* Delete Confirmation Dialog */}
{
Delete Product
- Are you sure you want to delete {product.name}? This action
- cannot be undone.
+ Are you sure you want to delete {product.name}? This action cannot be undone.
+ {/* Edit Product Modal */}
+
{/* ── Edit / Update Modal ── */}
@@ -410,10 +414,9 @@ const ProductCard = ({ product }) => {
name="name"
aria-label="Product Name"
value={updatedProduct.name}
- onChange={(e) =>
- setUpdatedProduct({ ...updatedProduct, name: e.target.value })
- }
+ onChange={(e) => setUpdatedProduct({ ...updatedProduct, name: e.target.value })}
/>
+
{
min={0}
aria-label="Price"
value={updatedProduct.price}
- onChange={(e) =>
- setUpdatedProduct({ ...updatedProduct, price: Number(e.target.value) })
- }
+ onChange={(e) => setUpdatedProduct({ ...updatedProduct, price: Number(e.target.value) })}
/>
@@ -446,11 +447,7 @@ const ProductCard = ({ product }) => {
aria-label="Image URL"
value={updatedProduct.imageFile ? "" : updatedProduct.image}
onChange={(e) => {
- setUpdatedProduct({
- ...updatedProduct,
- image: e.target.value,
- imageFile: null,
- });
+ setUpdatedProduct({ ...updatedProduct, image: e.target.value, imageFile: null });
setImagePreview(e.target.value || product.image);
if (fileInputRef.current) fileInputRef.current.value = "";
}}
@@ -469,13 +466,7 @@ const ProductCard = ({ product }) => {
/>
)}
-
+
Optional Details
@@ -484,9 +475,7 @@ const ProductCard = ({ product }) => {
name="description"
aria-label="Description"
value={updatedProduct.description || ""}
- onChange={(e) =>
- setUpdatedProduct({ ...updatedProduct, description: e.target.value })
- }
+ onChange={(e) => setUpdatedProduct({ ...updatedProduct, description: e.target.value })}
/>
{
name="category"
aria-label="Category"
value={updatedProduct.category || ""}
- onChange={(e) =>
- setUpdatedProduct({ ...updatedProduct, category: e.target.value })
- }
+ onChange={(e) => setUpdatedProduct({ ...updatedProduct, category: e.target.value })}
/>
{
name="brand"
aria-label="Brand"
value={updatedProduct.brand || ""}
- onChange={(e) =>
- setUpdatedProduct({ ...updatedProduct, brand: e.target.value })
- }
+ onChange={(e) => setUpdatedProduct({ ...updatedProduct, brand: e.target.value })}
/>
{
type="number"
aria-label="Stock Quantity"
value={updatedProduct.stock ?? ""}
- onChange={(e) =>
- setUpdatedProduct({
- ...updatedProduct,
- stock: e.target.value === "" ? "" : Number(e.target.value),
- })
- }
+ onChange={(e) => setUpdatedProduct({ ...updatedProduct, stock: e.target.value === "" ? "" : Number(e.target.value) })}
/>
{updatedProduct.stock != null && updatedProduct.stock !== '' && updatedProduct.stock <= LOW_STOCK_THRESHOLD && (
@@ -572,12 +552,21 @@ const ProductCard = ({ product }) => {
type="number"
aria-label="Original Price"
value={updatedProduct.originalPrice ?? ""}
- onChange={(e) =>
- setUpdatedProduct({
- ...updatedProduct,
- originalPrice: e.target.value === "" ? "" : Number(e.target.value),
- })
- }
+ onChange={(e) => setUpdatedProduct({ ...updatedProduct, originalPrice: e.target.value === "" ? "" : Number(e.target.value) })}
+ />
+
+ {/* ─── TAGS INPUT ────────────────────────────────────────── */}
+ {
+ const tagsArray = e.target.value
+ .split(',')
+ .map(tag => tag.trim())
+ .filter(tag => tag && tag.length >= 2 && tag.length <= 30);
+ setUpdatedProduct({ ...updatedProduct, tags: tagsArray.slice(0, 5) });
+ }}
/>
{
type="number"
aria-label="Discount Percentage"
value={updatedProduct.discount ?? ""}
- onChange={(e) =>
- setUpdatedProduct({
- ...updatedProduct,
- discount: e.target.value === "" ? "" : Number(e.target.value),
- })
- }
- />
-
- {/* Tags Input */}
- {
- const tagsArray = e.target.value
- .split(",")
- .map((tag) => tag.trim())
- .filter((tag) => tag && tag.length >= 2 && tag.length <= 30);
- setUpdatedProduct({ ...updatedProduct, tags: tagsArray });
- }}
+ onChange={(e) => setUpdatedProduct({ ...updatedProduct, discount: e.target.value === "" ? "" : Number(e.target.value) })}
/>
diff --git a/FRONTEND/src/pages/CreatePage.jsx b/FRONTEND/src/pages/CreatePage.jsx
index c7a85ef..b7644e6 100644
--- a/FRONTEND/src/pages/CreatePage.jsx
+++ b/FRONTEND/src/pages/CreatePage.jsx
@@ -112,6 +112,43 @@ const CreatePage = () => {
p={6} rounded={"lg"} shadow={"md"}
>
+ setNewProduct({ ...newProduct, name: e.target.value })}
+ />
+ setNewProduct({ ...newProduct, price: e.target.value })}
+ />
+ setNewProduct({ ...newProduct, image: e.target.value })}
+ />
+
+
+ {/* ─── TAGS INPUT ────────────────────────────────────────── */}
+ {
+ const tagsArray = e.target.value
+ .split(',')
+ .map(tag => tag.trim())
+ .filter(tag => tag && tag.length >= 2 && tag.length <= 30);
+ setNewProduct({ ...newProduct, tags: tagsArray.slice(0, 5) });
+ setNewProduct({ ...newProduct, tags: tagsArray });
+ }}
+ />
+
+