Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 152 additions & 14 deletions backend/src/controllers/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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" });
}
Expand All @@ -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)) {
Expand All @@ -101,6 +224,8 @@ export const addProduct = [
description,
userEmail,
images,
condition,
tags,
timeCreated: new Date(),
timeUpdated: new Date(),
});
Expand Down Expand Up @@ -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];

Expand All @@ -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 },
);

Expand Down
11 changes: 11 additions & 0 deletions backend/src/models/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const productSchema = new Schema({
},
description: {
type: String,
required: false,
},
timeCreated: {
type: Date,
Expand All @@ -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 }],
});

Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 0 additions & 3 deletions frontend/.env.development

This file was deleted.

111 changes: 111 additions & 0 deletions frontend/src/components/FilterSort.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="filter-bar">
{/* Price Range */}
<div className="filter-section">
<h3>Price Range</h3>
<input
type="number"
placeholder="Min"
value={filters.minPrice || ''}
onChange={(e) => setFilters({ ...filters, minPrice: e.target.value ? Number(e.target.value) : undefined })}
/>
<input
type="number"
placeholder="Max"
value={filters.maxPrice || ''}
onChange={(e) => setFilters({ ...filters, maxPrice: e.target.value ? Number(e.target.value) : undefined })}
/>
</div>

{/* Condition */}
<div className="filter-section">
<h3>Condition</h3>
<select
value={filters.condition || ''}
onChange={(e) => setFilters({ ...filters, condition: e.target.value })}
>
<option value="">All</option>
<option value="New">New</option>
<option value="Used">Used</option>
</select>
</div>

{/* Tags/Categories */}
<div className="filter-section">
<h3>Categories</h3>
<div className="tag-buttons">
{TAGS.map(tag => (
<button
key={tag}
className={filters.tags?.includes(tag) ? 'active' : ''}
onClick={() => handleTagToggle(tag)}
>
{tag}
</button>
))}
</div>
</div>

{/* Sorting */}
<div className="filter-section">
<h3>Sort By</h3>
<select
value={filters.sortBy || 'timeCreated'}
onChange={(e) => setFilters({ ...filters, sortBy: e.target.value })}
>
<option value="timeCreated">Date Posted</option>
<option value="price">Price</option>
</select>

<select
value={filters.order || 'desc'}
onChange={(e) => setFilters({ ...filters, order: e.target.value })}
>
<option value="desc">
{filters.sortBy === 'price' ? 'High to Low' :
filters.sortBy === 'timeCreated' ? 'Newest First' : 'Descending'}
</option>
<option value="asc">
{filters.sortBy === 'price' ? 'Low to High' :
filters.sortBy === 'timeCreated' ? 'Oldest First' : 'Ascending'}
</option>
</select>
</div>

<button onClick={clearFilters}>Clear All Filters</button>
</div>
);
}
Loading