diff --git a/backend/src/controllers/products.ts b/backend/src/controllers/products.ts index 3c00ae0..d06153c 100644 --- a/backend/src/controllers/products.ts +++ b/backend/src/controllers/products.ts @@ -16,11 +16,76 @@ const upload = multer({ }).array("images", 10); /** - * get all the products in database + * get all the products in database (keep filters, sorting in mind) */ export const getProducts = async (req: AuthenticatedRequest, res: Response) => { try { - const products = await ProductModel.find(); + const { sortBy, order, minPrice, maxPrice, condition, tags } = req.query; + + // object containing different filters we can apply + const filters: any = {}; + + // Check for filters and add them to object + if(minPrice || maxPrice) { + filters.price = {}; + if(minPrice) filters.price.$gte = Number(minPrice); + if(maxPrice) filters.price.$lte = Number(maxPrice); + } + + // Filter by specific condition + if(condition) { + filters.condition = condition; + } + + // Filter by category + if(tags) { + // Handle both single tag and multiple tags + let tagArray: string[]; + + if (Array.isArray(tags)) { + + // Already an array: ?tags=Electronics&tags=Furniture + tagArray = tags as string[]; + } else if (typeof tags === 'string') { + + // Single string, could be comma-separated: ?tags=Electronics,Furniture + tagArray = tags.includes(',') ? tags.split(',').map(t => t.trim()) : [tags]; + + } else { + tagArray = []; + } + + if (tagArray.length > 0) { + filters.tags = { $in: tagArray }; + } + } + + // sort object for different sorting options + const sortTypes: any = {} + + if(sortBy) { + const sortOrder = order === "asc" ? 1 : -1; + + switch(sortBy) { + case "price": + sortTypes.price = sortOrder; + break; + case "timeCreated": + sortTypes.timeCreated = sortOrder; + break; + case "condition": + sortTypes.condition = sortOrder; + break; + default: + // newest is default + sortTypes.timeCreated = -1; + } + } else { + // default sorting by newest + sortTypes.timeCreated = -1; + } + + const products = await ProductModel.find(filters).sort(sortTypes); res.status(200).json(products); } catch (error) { res.status(500).json({ message: "Error fetching products", error }); @@ -52,7 +117,63 @@ export const getProductById = async (req: AuthenticatedRequest, res: Response) = export const getProductsByName = async (req: AuthenticatedRequest, res: Response) => { try { const query = req.params.query; - const products = await ProductModel.find({ name: { $regex: query, $options: "i" } }); + const { sortBy, order, minPrice, maxPrice, condition, tags } = req.query; + + // Name is now a filter we can apply + const filters: any = { + name: { $regex: query, $options: "i" } + }; + + // price range + if (minPrice || maxPrice) { + filters.price = {}; + if (minPrice) filters.price.$gte = Number(minPrice); + if (maxPrice) filters.price.$lte = Number(maxPrice); + } + + // condition + if (condition) { + filters.condition = condition; + } + + // filter by category + if (tags) { + let tagArray: string[]; + + if (Array.isArray(tags)) { + tagArray = tags as string[]; + } else if (typeof tags === 'string') { + tagArray = tags.includes(',') ? tags.split(',').map(t => t.trim()) : [tags]; + } else { + tagArray = []; + } + + if (tagArray.length > 0) { + filters.tags = { $in: tagArray }; + } + } + + // Creates sorting options + const sortOptions: any = {}; + if (sortBy) { + const sortOrder = order === "asc" ? 1 : -1; + + switch (sortBy) { + case "price": + sortOptions.price = sortOrder; + break; + case "timeCreated": + sortOptions.timeCreated = sortOrder; + break; + default: + sortOptions.timeCreated = -1; + } + } else { + sortOptions.timeCreated = -1; + } + + const products = await ProductModel.find(filters).sort(sortOptions); + if (!products) { return res.status(404).json({ message: "Product not found" }); } @@ -63,19 +184,21 @@ export const getProductsByName = async (req: AuthenticatedRequest, res: Response }; /** - * add product to database thru name, price, description, and userEmail + * add product to database thru name, price, description, userEmail, and condition */ export const addProduct = [ upload, async (req: AuthenticatedRequest, res: Response) => { try { - const { name, price, description } = req.body; + const { name, price, description, category, condition } = req.body; if (!req.user) return res.status(404).json({ message: "User not found" }); const userId = req.user._id; const userEmail = req.user.userEmail; - if (!name || !price || !userEmail) { - return res.status(400).json({ message: "Name, price, and userEmail are required." }); + if (!name || !price || !userEmail || !condition) { + return res.status(400).json({ message: "Name, price, userEmail, and condition are required." }); } + + const tags = category ? [category] : []; const images: string[] = []; if (req.files && Array.isArray(req.files)) { @@ -101,6 +224,8 @@ export const addProduct = [ description, userEmail, images, + condition, + tags, timeCreated: new Date(), timeUpdated: new Date(), }); @@ -168,6 +293,12 @@ export const updateProductById = [ return res.status(400).json({ message: "User does not own this product" }); } + // handle tags input + let tags: string[] | undefined; + if (req.body.category) { + tags = [req.body.category]; + } + let existing = req.body.existingImages || []; if (!Array.isArray(existing)) existing = [existing]; @@ -183,15 +314,22 @@ export const updateProductById = [ const finalImages = [...existing, ...newUrls]; + const updateData: any = { + name: req.body.name, + price: req.body.price, + description: req.body.description, + condition: req.body.condition, + images: finalImages, + timeUpdated: new Date(), + }; + + if (tags) { + updateData.tags = tags; + } + const updatedProduct = await ProductModel.findByIdAndUpdate( id, - { - name: req.body.name, - price: req.body.price, - description: req.body.description, - images: finalImages, - timeUpdated: new Date(), - }, + updateData, { new: true }, ); diff --git a/backend/src/models/product.ts b/backend/src/models/product.ts index ca7386a..7ce1398 100644 --- a/backend/src/models/product.ts +++ b/backend/src/models/product.ts @@ -11,6 +11,7 @@ const productSchema = new Schema({ }, description: { type: String, + required: false, }, timeCreated: { type: Date, @@ -24,6 +25,16 @@ const productSchema = new Schema({ type: String, required: true, }, + tags: { + type: [String], + enum: ['Electronics', 'School Supplies', 'Dorm Essentials', 'Furniture', 'Clothes', 'Miscellaneous'], + required: false + }, + condition: { + type: String, + enum: ["New", "Like New", "Used", "For Parts"], + required: true, + }, images: [{ type: String }], }); diff --git a/backend/src/routes/product.ts b/backend/src/routes/product.ts index 581fb50..3945a82 100644 --- a/backend/src/routes/product.ts +++ b/backend/src/routes/product.ts @@ -11,8 +11,8 @@ import { authenticateUser } from "src/validators/authUserMiddleware"; const router = express.Router(); router.get("/", authenticateUser, getProducts); -router.get("/:id", authenticateUser, getProductById); router.get("/search/:query", authenticateUser, getProductsByName); +router.get("/:id", authenticateUser, getProductById); router.post("/", authenticateUser, addProduct); router.delete("/:id", authenticateUser, deleteProductById); router.patch("/:id", authenticateUser, updateProductById); diff --git a/frontend/.env.development b/frontend/.env.development deleted file mode 100644 index 3cd2602..0000000 --- a/frontend/.env.development +++ /dev/null @@ -1,3 +0,0 @@ -# Don't stop the React webpack build if there are lint errors. -ESLINT_NO_DEV_ERRORS=true -VITE_API_BASE_URL=http://localhost:5000 diff --git a/frontend/src/components/FilterSort.tsx b/frontend/src/components/FilterSort.tsx new file mode 100644 index 0000000..0cbad05 --- /dev/null +++ b/frontend/src/components/FilterSort.tsx @@ -0,0 +1,111 @@ +import React from 'react'; + +interface FilterBarProps { + filters: any; + setFilters: (filters: any) => void; +} + +const TAGS = [ + 'Electronics', + 'School Supplies', + 'Dorm Essentials', + 'Furniture', + 'Clothes', + 'Miscellaneous' +]; + +export default function FilterBar({ filters, setFilters }: FilterBarProps) { + const handleTagToggle = (tag: string) => { + const currentTags = filters.tags || []; + const newTags = currentTags.includes(tag) + ? currentTags.filter((t: string) => t !== tag) + : [...currentTags, tag]; + + setFilters({ ...filters, tags: newTags }); + }; + + const clearFilters = () => { + setFilters({ + sortBy: 'timeCreated', + order: 'desc' + }); + }; + + return ( +
Add Product