From d937c187f2d2d89e115163c027833ec28ab409af Mon Sep 17 00:00:00 2001 From: Taha Qamar Date: Tue, 27 Jan 2026 22:05:56 -0800 Subject: [PATCH 1/4] finishes search feature with multiple filters and sorting --- backend/src/controllers/products.ts | 108 ++++++++-- backend/src/models/product.ts | 11 + backend/src/routes/product.ts | 2 +- frontend/src/components/FilterSort.tsx | 111 ++++++++++ frontend/src/components/SearchBar.tsx | 42 +--- frontend/src/pages/AddProduct.tsx | 169 ++++++++++++--- frontend/src/pages/EditProduct.tsx | 203 ++++++++++++++---- .../src/pages/Individual-product-page.tsx | 13 ++ frontend/src/pages/Marketplace.tsx | 69 +++++- 9 files changed, 604 insertions(+), 124 deletions(-) create mode 100644 frontend/src/components/FilterSort.tsx diff --git a/backend/src/controllers/products.ts b/backend/src/controllers/products.ts index 3c00ae0..a5da31d 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 tags + 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 }); @@ -63,19 +128,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 +168,8 @@ export const addProduct = [ description, userEmail, images, + condition, + tags, timeCreated: new Date(), timeUpdated: new Date(), }); @@ -168,6 +237,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 +258,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/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 ( +
+ {/* Price Range */} +
+

Price Range

+ setFilters({ ...filters, minPrice: e.target.value ? Number(e.target.value) : undefined })} + /> + setFilters({ ...filters, maxPrice: e.target.value ? Number(e.target.value) : undefined })} + /> +
+ + {/* Condition */} +
+

Condition

+ +
+ + {/* Tags/Categories */} +
+

Categories

+
+ {TAGS.map(tag => ( + + ))} +
+
+ + {/* Sorting */} +
+

Sort By

+ + + +
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index aa40a0f..80d2ee6 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -1,50 +1,22 @@ -import { useEffect, useState } from "react"; -import { get } from "src/api/requests"; +import { useState } from "react"; interface Props { - setProducts: (products: []) => void; + setProducts: (query: string) => void; setError: (error: string) => void; } export default function SearchBar({ setProducts, setError }: Props) { const [query, setQuery] = useState(null); - useEffect(() => { - /* - * if query is null, get all products - * otherwise get products that match the query - */ - const search = async () => { - try { - if (query && query.trim().length > 0) { - await get(`/api/products/search/${query}`).then((res) => { - if (res.ok) { - res.json().then((data) => { - setProducts(data); - }); - } - }); - } else { - await get(`/api/products/`).then((res) => { - if (res.ok) { - res.json().then((data) => { - setProducts(data); - }); - } - }); - } - } catch (err) { - setError("Unable to display products. Try again later."); - console.error(err); - } - }; - search(); - }, [query]); + const handleChange = (value: string) => { + setQuery(value); + setProducts(value); + }; return ( setQuery(e.target.value)} + onChange={(e) => handleChange(e.target.value)} placeholder="Search for a product..." className="w-full bg-[#F8F8F8] shadow-md p-3 px-6 mx-auto my-2 rounded-3xl" /> diff --git a/frontend/src/pages/AddProduct.tsx b/frontend/src/pages/AddProduct.tsx index bf68a9b..87cdc66 100644 --- a/frontend/src/pages/AddProduct.tsx +++ b/frontend/src/pages/AddProduct.tsx @@ -10,8 +10,26 @@ export function AddProduct() { const productName = useRef(null); const productPrice = useRef(null); const productDescription = useRef(null); + const productYear = useRef(null); + const productCategory = useRef(null); + const productCondition = useRef(null); const productImages = useRef(null); + + const currentYear = new Date().getFullYear(); + const years = Array.from({ length: currentYear - 1950 }, (_, i) => currentYear - i); + + + const categories = [ + 'Electronics', + 'School Supplies', + 'Dorm Essentials', + 'Furniture', + 'Clothes', + 'Miscellaneous']; + + const conditions = ["New", "Used"]; + const { user } = useContext(FirebaseContext); const [error, setError] = useState(false); const [fileError, setFileError] = useState(null); @@ -56,7 +74,7 @@ export function AddProduct() { setIsSubmitting(true); e.preventDefault(); try { - if (productName.current && productPrice.current && productDescription.current && user) { + if (productName.current && productPrice.current && productDescription.current && productYear.current && productCategory.current && productCondition.current && user) { let images; if (productImages.current && productImages.current.files) { images = productImages.current.files[0]; @@ -66,6 +84,9 @@ export function AddProduct() { body.append("name", productName.current.value); body.append("price", productPrice.current.value); body.append("description", productDescription.current.value); + body.append("year", productYear.current.value); + body.append("category", productCategory.current.value); + body.append("condition", productCondition.current.value); if (user.email) body.append("userEmail", user.email); if (productImages.current && productImages.current.files) { @@ -96,6 +117,66 @@ export function AddProduct() {

Add Product

+ {/* Images */} +
+ +

Upload up to 10 photos

+ + {newPreviews.length > 0 && ( +
+
+ {newPreviews.map((src, idx) => ( +
+ + +
+ ))} +
+
+ )} + + +
{/* Name */}
- {/* Images */} + {/* Year */}
-
- {newPreviews.length > 0 && ( -
-
- {newPreviews.map((src, idx) => ( -
- - -
- ))} -
-
- )} + {/* Category */} +
+ + +
- + + +
+ {/* Images */} +
+ +

Upload up to 10 photos

+ + {(newPreviews.length > 0 || existingImages.length > 0) && ( +
+ {existingImages.map((url) => ( +
+ + +
+ ))} + + {newPreviews.map((src, idx) => ( +
+ + +
+ ))} +
+ )} + + +
+
-
- {(newPreviews.length > 0 || existingImages.length > 0) && ( -
- {existingImages.map((url) => ( -
- - -
- ))} - - {newPreviews.map((src, idx) => ( -
- - -
- ))} -
- )} +
+ + +
- + +
@@ -257,4 +376,4 @@ export function EditProduct() { ); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Individual-product-page.tsx b/frontend/src/pages/Individual-product-page.tsx index a54eb83..a36e59d 100644 --- a/frontend/src/pages/Individual-product-page.tsx +++ b/frontend/src/pages/Individual-product-page.tsx @@ -20,6 +20,7 @@ export function IndividualProductPage() { images: string[]; userEmail: string; description: string; + tags: string[]; }>(); const [error, setError] = useState(); const [hasPermissions, setHasPermissions] = useState(false); @@ -244,6 +245,18 @@ export function IndividualProductPage() {

)} + {product?.tags && ( +
+ {product.tags.map((tag) => ( +
+ {tag} +
+ ))} +
+ )} {!hasPermissions && (
setIsHovered(true)} diff --git a/frontend/src/pages/Marketplace.tsx b/frontend/src/pages/Marketplace.tsx index a80effd..e1a9fee 100644 --- a/frontend/src/pages/Marketplace.tsx +++ b/frontend/src/pages/Marketplace.tsx @@ -2,9 +2,19 @@ import { useState, useEffect, useContext } from "react"; import { Helmet } from "react-helmet-async"; import Product from "src/components/Product"; import SearchBar from "src/components/SearchBar"; +import FilterSort from "src/components/FilterSort"; import { FirebaseContext } from "src/utils/FirebaseProvider"; import { get, post } from "src/api/requests"; +interface FilterState { + minPrice?: number; + maxPrice?: number; + condition?: "New" | "Used" | ""; + tags?: string[]; + sortBy?: "price" | "timeCreated"; + order?: "asc" | "desc"; +} + export function Marketplace() { const [products, setProducts] = useState< Array<{ @@ -15,6 +25,13 @@ export function Marketplace() { }> >([]); const [error, setError] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); + const [filters, setFilters] = useState({ + sortBy: "timeCreated", + order: "desc", + tags: [], + }); + const { user } = useContext(FirebaseContext); const [savedProducts, setSavedProducts] = useState([]); @@ -44,6 +61,53 @@ export function Marketplace() { } }; + const fetchProducts = async () => { + try { + setError(""); + + // If there's a search query, use search endpoint + if (searchQuery && searchQuery.trim().length > 0) { + const res = await get(`/api/products/search/${searchQuery}`); + if (res.ok) { + const data = await res.json(); + setProducts(data); + } else { + setError("Unable to search products. Try again later."); + } + return; + } + + // Otherwise, use filter/sort endpoint + const params = new URLSearchParams(); + + if (filters.minPrice) params.append("minPrice", filters.minPrice.toString()); + if (filters.maxPrice) params.append("maxPrice", filters.maxPrice.toString()); + if (filters.condition) params.append("condition", filters.condition); + if (filters.tags && filters.tags.length > 0) { + filters.tags.forEach((tag) => params.append("tags", tag)); + } + if (filters.sortBy) params.append("sortBy", filters.sortBy); + if (filters.order) params.append("order", filters.order); + + const queryString = params.toString(); + const res = await get(`/api/products${queryString ? `?${queryString}` : ""}`); + + if (res.ok) { + const data = await res.json(); + setProducts(data); + } else { + setError("Unable to display products. Try again later."); + } + } catch (err) { + setError("Unable to display products. Try again later."); + console.error(err); + } + }; + + useEffect(() => { + fetchProducts(); + }, [filters, searchQuery]); + useEffect(() => { fetchSavedProducts(); }, [user]); @@ -64,7 +128,10 @@ export function Marketplace() { Add Product
- + + + + {error &&

{error}

} {!error && products?.length === 0 && (

No products available

From 955dbc1ba04f4d7a451afd0ec21522fc0206c75b Mon Sep 17 00:00:00 2001 From: Taha Qamar Date: Wed, 4 Feb 2026 18:22:58 -0800 Subject: [PATCH 2/4] fixes bug with searching and filtering at the same time --- backend/src/controllers/products.ts | 60 ++++++++++++++++++++++++++++- frontend/.env.development | 3 -- frontend/src/pages/Marketplace.tsx | 29 +++++++------- 3 files changed, 74 insertions(+), 18 deletions(-) delete mode 100644 frontend/.env.development diff --git a/backend/src/controllers/products.ts b/backend/src/controllers/products.ts index a5da31d..d06153c 100644 --- a/backend/src/controllers/products.ts +++ b/backend/src/controllers/products.ts @@ -37,7 +37,7 @@ export const getProducts = async (req: AuthenticatedRequest, res: Response) => { filters.condition = condition; } - // Filter by tags + // Filter by category if(tags) { // Handle both single tag and multiple tags let tagArray: string[]; @@ -117,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" }); } 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/pages/Marketplace.tsx b/frontend/src/pages/Marketplace.tsx index e1a9fee..85f8333 100644 --- a/frontend/src/pages/Marketplace.tsx +++ b/frontend/src/pages/Marketplace.tsx @@ -65,19 +65,7 @@ export function Marketplace() { try { setError(""); - // If there's a search query, use search endpoint - if (searchQuery && searchQuery.trim().length > 0) { - const res = await get(`/api/products/search/${searchQuery}`); - if (res.ok) { - const data = await res.json(); - setProducts(data); - } else { - setError("Unable to search products. Try again later."); - } - return; - } - - // Otherwise, use filter/sort endpoint + // Search a part of filter/sort endpoint const params = new URLSearchParams(); if (filters.minPrice) params.append("minPrice", filters.minPrice.toString()); @@ -90,6 +78,21 @@ export function Marketplace() { if (filters.order) params.append("order", filters.order); const queryString = params.toString(); + + // Use in case of search with filters + if (searchQuery && searchQuery.trim().length > 0) { + const res = await get( + `/api/products/search/${searchQuery}${queryString ? `?${queryString}` : ""}` + ); + if (res.ok) { + const data = await res.json(); + setProducts(data); + } else { + setError("Unable to search products. Try again later."); + } + return; + } + const res = await get(`/api/products${queryString ? `?${queryString}` : ""}`); if (res.ok) { From 6dfecb955682f65d35a6d60fad2e4759103ec890 Mon Sep 17 00:00:00 2001 From: Taha Qamar Date: Wed, 18 Feb 2026 05:29:31 -0800 Subject: [PATCH 3/4] adds sidebar and filtering/categories dropdown, seperates sorting under search bar to match ui --- frontend/src/components/FilterSort.tsx | 273 ++++++++++++++++++------- frontend/src/pages/Marketplace.tsx | 138 +++++++++---- 2 files changed, 289 insertions(+), 122 deletions(-) diff --git a/frontend/src/components/FilterSort.tsx b/frontend/src/components/FilterSort.tsx index 0cbad05..4f896f4 100644 --- a/frontend/src/components/FilterSort.tsx +++ b/frontend/src/components/FilterSort.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; interface FilterBarProps { filters: any; @@ -11,101 +11,220 @@ const TAGS = [ 'Dorm Essentials', 'Furniture', 'Clothes', - 'Miscellaneous' + 'Miscellaneous', ]; +function Chevron({ open }: { open: boolean }) { + return ( + + + + ); +} + +function TreeItem({ + label, + isLast, + active, + onClick, + right, +}: { + label: string; + isLast?: boolean; + active?: boolean; + onClick: () => void; + right?: React.ReactNode; +}) { + return ( +
+ {/* Vertical gold spine — clips at midpoint for last item */} +
+ +
+ ); +} + export default function FilterBar({ filters, setFilters }: FilterBarProps) { + const [categoryOpen, setCategoryOpen] = useState(true); + const [filterOpen, setFilterOpen] = useState(true); + const [conditionOpen, setConditionOpen] = useState(false); + const [priceOpen, setPriceOpen] = useState(false); + 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' - }); + setFilters({ sortBy: 'timeCreated', order: 'desc', tags: [] }); }; + const sectionHeaderClass = + 'flex items-center gap-2 w-full py-2 hover:opacity-80 transition-opacity'; + const iconBoxClass = + 'flex items-center justify-center w-5 h-5 rounded border border-ucsd-blue text-ucsd-blue shrink-0'; + return ( -
- {/* Price Range */} -
-

Price Range

- setFilters({ ...filters, minPrice: e.target.value ? Number(e.target.value) : undefined })} - /> - setFilters({ ...filters, maxPrice: e.target.value ? Number(e.target.value) : undefined })} - /> -
- - {/* Condition */} -
-

Condition

- -
- - {/* Tags/Categories */} -
-

Categories

-
- {TAGS.map(tag => ( - + + {categoryOpen && ( +
+ {TAGS.map((tag, i) => ( + handleTagToggle(tag)} - > - {tag} - + /> ))}
-
- - {/* Sorting */} -
-

Sort By

- - - -
- - + )} + + {/* ══ FILTER ══ */} + + + {filterOpen && ( +
+ + {/* Condition */} + setConditionOpen((o) => !o)} + right={} + /> + {conditionOpen && ( +
+ +
+ )} + + {/* Price Range */} + setPriceOpen((o) => !o)} + right={} + /> + {priceOpen && ( +
+ + {/* Row for Min / Max */} +
+ + setFilters({ + ...filters, + minPrice: e.target.value ? Number(e.target.value) : undefined, + }) + } + /> + + + setFilters({ + ...filters, + maxPrice: e.target.value ? Number(e.target.value) : undefined, + }) + } + /> +
+
+ )} + + +
+ )} + + {/* Clear Filters */} +
); } \ No newline at end of file diff --git a/frontend/src/pages/Marketplace.tsx b/frontend/src/pages/Marketplace.tsx index 85f8333..e7068c6 100644 --- a/frontend/src/pages/Marketplace.tsx +++ b/frontend/src/pages/Marketplace.tsx @@ -25,7 +25,7 @@ export function Marketplace() { }> >([]); const [error, setError] = useState(""); - const [searchQuery, setSearchQuery] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); const [filters, setFilters] = useState({ sortBy: "timeCreated", order: "desc", @@ -65,9 +65,7 @@ export function Marketplace() { try { setError(""); - // Search a part of filter/sort endpoint const params = new URLSearchParams(); - if (filters.minPrice) params.append("minPrice", filters.minPrice.toString()); if (filters.maxPrice) params.append("maxPrice", filters.maxPrice.toString()); if (filters.condition) params.append("condition", filters.condition); @@ -79,14 +77,12 @@ export function Marketplace() { const queryString = params.toString(); - // Use in case of search with filters if (searchQuery && searchQuery.trim().length > 0) { const res = await get( `/api/products/search/${searchQuery}${queryString ? `?${queryString}` : ""}` ); if (res.ok) { - const data = await res.json(); - setProducts(data); + setProducts(await res.json()); } else { setError("Unable to search products. Try again later."); } @@ -94,10 +90,8 @@ export function Marketplace() { } const res = await get(`/api/products${queryString ? `?${queryString}` : ""}`); - if (res.ok) { - const data = await res.json(); - setProducts(data); + setProducts(await res.json()); } else { setError("Unable to display products. Try again later."); } @@ -107,57 +101,111 @@ export function Marketplace() { } }; - useEffect(() => { - fetchProducts(); - }, [filters, searchQuery]); - - useEffect(() => { - fetchSavedProducts(); - }, [user]); + useEffect(() => { fetchProducts(); }, [filters, searchQuery]); + useEffect(() => { fetchSavedProducts(); }, [user]); return ( <> Low-Price Center Marketplace -
-
-
-

Marketplace

+ +
+
+ + {/* ── Page header ── */} +
+

+ Marketplace +

- - - - - {error &&

{error}

} - {!error && products?.length === 0 && ( -

No products available

- )} -
- {products.map((product) => ( -
- + + {/* ── White card ── */} +
+ + {/* LEFT SIDEBAR */} +
+ +
+ + {/* RIGHT CONTENT */} +
+ + {/* Search + Sort row */} +
+ {/* SearchBar expands to fill space */} +
+ +
+ + {/* Sort controls */} +
+ + Sort: + + + + +
- ))} + + {/* Status messages */} + {error && ( +

{error}

+ )} + {!error && products?.length === 0 && ( +

No products available

+ )} + + {/* Product grid */} +
+ {products.map((product) => ( + + ))} +
+ +
); -} +} \ No newline at end of file From 79936a4f2f924a98e524cc0bb40edced81a7b4a3 Mon Sep 17 00:00:00 2001 From: Taha Qamar Date: Wed, 18 Feb 2026 15:09:28 -0800 Subject: [PATCH 4/4] fixes sidebar and adds LowPriceCenter, changed colors to match figma design in config file --- frontend/src/components/FilterSort.tsx | 327 ++++++++++----------- frontend/src/components/FilterSort_old.tsx | 232 +++++++++++++++ frontend/src/pages/Marketplace.tsx | 102 ++++--- frontend/tailwind.config.js | 8 +- 4 files changed, 459 insertions(+), 210 deletions(-) create mode 100644 frontend/src/components/FilterSort_old.tsx diff --git a/frontend/src/components/FilterSort.tsx b/frontend/src/components/FilterSort.tsx index 4f896f4..9fd5317 100644 --- a/frontend/src/components/FilterSort.tsx +++ b/frontend/src/components/FilterSort.tsx @@ -14,65 +14,27 @@ const TAGS = [ 'Miscellaneous', ]; -function Chevron({ open }: { open: boolean }) { +function Chevron({ open, className = '' }: { open: boolean; className?: string }) { return ( - + ); } -function TreeItem({ - label, - isLast, - active, - onClick, - right, -}: { - label: string; - isLast?: boolean; - active?: boolean; - onClick: () => void; - right?: React.ReactNode; -}) { +function RightChevron({ className = '' }: { className?: string }) { return ( -
- {/* Vertical gold spine — clips at midpoint for last item */} -
- -
+ + + ); } @@ -80,7 +42,7 @@ export default function FilterBar({ filters, setFilters }: FilterBarProps) { const [categoryOpen, setCategoryOpen] = useState(true); const [filterOpen, setFilterOpen] = useState(true); const [conditionOpen, setConditionOpen] = useState(false); - const [priceOpen, setPriceOpen] = useState(false); + const [priceOpen, setPriceOpen] = useState(true); const handleTagToggle = (tag: string) => { const currentTags = filters.tags || []; @@ -94,134 +56,169 @@ export default function FilterBar({ filters, setFilters }: FilterBarProps) { setFilters({ sortBy: 'timeCreated', order: 'desc', tags: [] }); }; - const sectionHeaderClass = - 'flex items-center gap-2 w-full py-2 hover:opacity-80 transition-opacity'; - const iconBoxClass = - 'flex items-center justify-center w-5 h-5 rounded border border-ucsd-blue text-ucsd-blue shrink-0'; - return ( -
+
{/* ══ CATEGORY ══ */} - - - {categoryOpen && ( -
- {TAGS.map((tag, i) => ( - handleTagToggle(tag)} - /> - ))} -
- )} +
+ + + {categoryOpen && ( +
+ {/* Continuous gold vertical spine */} +
+ + {TAGS.map((tag, i) => { + const active = filters.tags?.includes(tag); + const isLast = i === TAGS.length - 1; + return ( +
+ {/* Clip spine past midpoint of last row */} + {isLast && ( +
+ )} + +
+ ); + })} +
+ )} +
{/* ══ FILTER ══ */} - +
+ + + {filterOpen && ( +
+ {/* Gold spine — only covers the two items, clipped at last */} +
- {filterOpen && ( -
- - {/* Condition */} - setConditionOpen((o) => !o)} - right={} - /> - {conditionOpen && ( -
- -
- )} - - {/* Price Range */} - setPriceOpen((o) => !o)} - right={} - /> - {priceOpen && ( -
- - {/* Row for Min / Max */} -
- - setFilters({ - ...filters, - minPrice: e.target.value ? Number(e.target.value) : undefined, - }) - } - /> - - - setFilters({ - ...filters, - maxPrice: e.target.value ? Number(e.target.value) : undefined, - }) - } - /> + + + Condition + + + + {conditionOpen && ( +
+
+ )}
- )} - -
- )} + {/* Price Range */} +
+ {/* Clip gold spine after midpoint */} +
+ + {priceOpen && ( +
+ {/* Min — Max inline like mockup */} +
+ + setFilters({ ...filters, minPrice: e.target.value ? Number(e.target.value) : undefined }) + } + /> + + + setFilters({ ...filters, maxPrice: e.target.value ? Number(e.target.value) : undefined }) + } + /> +
+
+ )} +
+
+ )} +
{/* Clear Filters */} diff --git a/frontend/src/components/FilterSort_old.tsx b/frontend/src/components/FilterSort_old.tsx new file mode 100644 index 0000000..117ec48 --- /dev/null +++ b/frontend/src/components/FilterSort_old.tsx @@ -0,0 +1,232 @@ +import React, { useState } from 'react'; + +interface FilterBarProps { + filters: any; + setFilters: (filters: any) => void; +} + +const TAGS = [ + 'Electronics', + 'School Supplies', + 'Dorm Essentials', + 'Furniture', + 'Clothes', + 'Miscellaneous', +]; + +function Chevron({ open }: { open: boolean }) { + return ( + + + + ); +} + +function TreeItem({ + label, + isLast, + active, + onClick, + right, +}: { + label: string; + isLast?: boolean; + active?: boolean; + onClick: () => void; + right?: React.ReactNode; +}) { + return ( +
+ {/* Vertical gold spine — clips at midpoint for last item */} +
+ +
+ ); +} + +export default function FilterBar({ filters, setFilters }: FilterBarProps) { + const [categoryOpen, setCategoryOpen] = useState(true); + const [filterOpen, setFilterOpen] = useState(true); + const [conditionOpen, setConditionOpen] = useState(false); + const [priceOpen, setPriceOpen] = useState(false); + + 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', tags: [] }); + }; + + const sectionHeaderClass = + 'flex items-center gap-2 w-full py-2 hover:opacity-80 transition-opacity'; + const iconBoxClass = + 'flex items-center justify-center w-5 h-5 rounded border border-ucsd-blue text-ucsd-blue shrink-0'; + + return ( +
+ + {/* ══ CATEGORY ══ */} + + + {categoryOpen && ( +
+ {TAGS.map((tag, i) => ( + handleTagToggle(tag)} + /> + ))} +
+ )} + + {/* ══ FILTER ══ */} + + + {filterOpen && ( +
+ + {/* Condition */} + setConditionOpen((o) => !o)} + right={} + /> + {conditionOpen && ( +
+ +
+ )} + + {/* Price Range */} + setPriceOpen((o) => !o)} + right={} + /> + {priceOpen && ( +
+ + {/* Row for Min / Max */} +
+ + setFilters({ + ...filters, + minPrice: e.target.value ? Number(e.target.value) : undefined, + }) + } + /> + + + setFilters({ + ...filters, + maxPrice: e.target.value ? Number(e.target.value) : undefined, + }) + } + /> +
+ +
+ )} + + +
+ )} + + {/* Clear Filters */} + +
+ ); +} + diff --git a/frontend/src/pages/Marketplace.tsx b/frontend/src/pages/Marketplace.tsx index e7068c6..07e376b 100644 --- a/frontend/src/pages/Marketplace.tsx +++ b/frontend/src/pages/Marketplace.tsx @@ -113,8 +113,8 @@ export function Marketplace() {
- {/* ── Page header ── */} -
+ {/* Outer page header: Marketplace title + Add Product */} +

Marketplace

@@ -127,30 +127,32 @@ export function Marketplace() {
{/* ── White card ── */} -
- - {/* LEFT SIDEBAR */} -
- -
- - {/* RIGHT CONTENT */} -
+
+ + {/* ── TOP BANNER ROW ── + Left: "Low Price Center" heading blended into white + Right: Search bar + sort controls stacked + */} +
+ + {/* Low Price Center — "Low" in ucsd-blue, "Price Center" in ucsd-gold, blended white bg */} +
+ Low + Price Center +
- {/* Search + Sort row */} -
- {/* SearchBar expands to fill space */} -
+ {/* Search bar + sort stacked on the right */} +
+ {/* Search bar */} +
- {/* Sort controls */} -
- - Sort: - + {/* Sort row — right-aligned, underneath search */} +
+ Sort:
+
+ + {/* Thin gold divider under the top banner */} +
- {/* Status messages */} - {error && ( -

{error}

- )} - {!error && products?.length === 0 && ( -

No products available

- )} - - {/* Product grid */} -
- {products.map((product) => ( - - ))} + {/* ── BOTTOM SECTION: Sidebar | Products ── */} +
+ + {/* LEFT SIDEBAR — fixed width, gold right border */} +
+ +
+ + {/* RIGHT: Product grid */} +
+ {error && ( +

{error}

+ )} + {!error && products?.length === 0 && ( +

No products available

+ )} + +
+ {products.map((product) => ( + + ))} +
+
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 1247a45..1d48fea 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -9,9 +9,13 @@ export default { xxl: "1300px", }, colors: { - "ucsd-blue": "#00629B", + // Figma mockup uses different blue + // "ucsd-blue": "#00629B", + "ucsd-blue": "#0E7395", "ucsd-darkblue": "#182B49", - "ucsd-gold": "#FFCD00", + // Figma mockup uses different gold + // "ucsd-gold": "#FFCD00", + "ucsd-gold": "#F6AE2D", }, fontFamily: { jetbrains: ["JetBrains Mono", "monospace"],