Skip to content
Draft
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
1,182 changes: 1,182 additions & 0 deletions backend/bun.lock

Large diffs are not rendered by default.

56 changes: 23 additions & 33 deletions backend/src/controllers/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,36 @@ const upload = multer({
*/
export const getProducts = async (req: AuthenticatedRequest, res: Response) => {
try {
const products = await ProductModel.find({ isMarkedSold: { $in: [false, null] } });
const { sortBy, order, minPrice, maxPrice, condition, tags } = req.query;

// object containing different filters we can apply
const filters: any = {};

const filters: any = {
isMarkedSold: { $in: [false, null] },
};

// Check for filters and add them to object
if(minPrice || maxPrice) {
if (minPrice || maxPrice) {
filters.price = {};
if(minPrice) filters.price.$gte = Number(minPrice);
if(maxPrice) filters.price.$lte = Number(maxPrice);
if (minPrice) filters.price.$gte = Number(minPrice);
if (maxPrice) filters.price.$lte = Number(maxPrice);
}

// Filter by specific condition
if(condition) {
if (condition) {
filters.condition = condition;
}

// Filter by category
if(tags) {
if (tags) {
// Handle both single tag and multiple tags
let tagArray: string[];

if (Array.isArray(tags)) {

if (Array.isArray(tags)) {
// Already an array: ?tags=Electronics&tags=Furniture
tagArray = tags as string[];
} else if (typeof tags === '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];

tagArray = tags.includes(",") ? tags.split(",").map((t) => t.trim()) : [tags];
} else {
tagArray = [];
}
Expand All @@ -62,12 +60,12 @@ export const getProducts = async (req: AuthenticatedRequest, res: Response) => {
}

// sort object for different sorting options
const sortTypes: any = {}
const sortTypes: any = {};

if(sortBy) {
if (sortBy) {
const sortOrder = order === "asc" ? 1 : -1;
switch(sortBy) {

switch (sortBy) {
case "price":
sortTypes.price = sortOrder;
break;
Expand Down Expand Up @@ -143,9 +141,11 @@ export const addProduct = [
const userId = req.user._id;
const userEmail = req.user.userEmail;
if (!name || !price || !userEmail || !condition) {
return res.status(400).json({ message: "Name, price, userEmail, and condition are required." });
return res
.status(400)
.json({ message: "Name, price, userEmail, and condition are required." });
}

const tags = category ? [category] : [];

const images: string[] = [];
Expand Down Expand Up @@ -275,19 +275,9 @@ export const updateProductById = [
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(),
isMarkedSold: req.body.isMarkedSold ?? false,
},
updateData,
{ new: true },
);
updateData.isMarkedSold = req.body.isMarkedSold ?? false;

const updatedProduct = await ProductModel.findByIdAndUpdate(id, updateData, { new: true });

if (!updatedProduct) {
return res.status(404).json({ message: "Product not found" });
Expand Down
14 changes: 11 additions & 3 deletions backend/src/models/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@ const productSchema = new Schema({
type: Boolean,
required: true,
default: false,
},
tags: {
type: [String],
enum: ['Electronics', 'School Supplies', 'Dorm Essentials', 'Furniture', 'Clothes', 'Miscellaneous'],
required: false
type: [String],
enum: [
"Electronics",
"School Supplies",
"Dorm Essentials",
"Furniture",
"Clothes",
"Miscellaneous",
],
required: false,
},
condition: {
type: String,
Expand Down
1,362 changes: 1,362 additions & 0 deletions frontend/bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@react-google-maps/api": "^2.20.8",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"firebase": "^11.0.1",
Expand Down
108 changes: 108 additions & 0 deletions frontend/src/components/ListingMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { GoogleMap, MarkerF, useJsApiLoader } from "@react-google-maps/api";

type ListingMapProps = {
center: { lat: number; lng: number };
markerTitle: string;
label: string;
className?: string;
};

const googleMapsApiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
const mapContainerStyle = {
width: "100%",
height: "100%",
};
const mapOptions = {
disableDefaultUI: true,
fullscreenControl: false,
mapTypeControl: false,
scrollwheel: false,
streetViewControl: false,
zoomControl: true,
};

function MapFallback({
className = "",
label,
message,
}: {
className?: string;
label: string;
message: string;
}) {
return (
<div
className={`rounded-2xl border border-gray-200 bg-[#F8F8F8] p-5 shadow-sm ${className}`.trim()}
>
<p className="font-inter text-sm font-semibold text-[#182B49]">{label}</p>
<p className="mt-2 font-inter text-sm leading-6 text-[#4B5563]">{message}</p>
</div>
);
}

export default function ListingMap({
center,
markerTitle,
label,
className = "",
}: ListingMapProps) {
if (!googleMapsApiKey) {
return (
<MapFallback
className={className}
label={label}
message="Add VITE_GOOGLE_MAPS_API_KEY to view the interactive map for this pickup area."
/>
);
}

return (
<LoadedListingMap
center={center}
className={className}
label={label}
markerTitle={markerTitle}
/>
);
}

function LoadedListingMap({ center, markerTitle, label, className = "" }: ListingMapProps) {
const { isLoaded, loadError } = useJsApiLoader({
googleMapsApiKey,
id: "listing-map-script",
});

if (loadError) {
return (
<MapFallback
className={className}
label={label}
message="The map could not be loaded right now. The pickup area is currently set to UCSD Price Center."
/>
);
}

if (!isLoaded) {
return (
<div
className={`h-64 animate-pulse rounded-2xl border border-gray-200 bg-[#F8F8F8] ${className}`.trim()}
aria-label="Loading pickup map"
/>
);
}

return (
<div
className={`h-64 overflow-hidden rounded-2xl border border-gray-200 shadow-sm ${className}`.trim()}
>
<GoogleMap
center={center}
mapContainerStyle={mapContainerStyle}
options={mapOptions}
zoom={16}
>
<MarkerF position={center} title={markerTitle} />
</GoogleMap>
</div>
);
}
Loading