Skip to content
Merged
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
70 changes: 13 additions & 57 deletions backend/src/controllers/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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
Expand Down Expand Up @@ -117,63 +118,10 @@ export const getProductById = async (req: AuthenticatedRequest, res: Response) =
export const getProductsByName = async (req: AuthenticatedRequest, res: Response) => {
try {
const query = req.params.query;
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);

const products = await ProductModel.find({
name: { $regex: query, $options: "i" },
isMarkedSold: { $in: [false, null] },
});
if (!products) {
return res.status(404).json({ message: "Product not found" });
}
Expand Down Expand Up @@ -329,6 +277,14 @@ export const updateProductById = [

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 },
);
Expand Down
4 changes: 4 additions & 0 deletions backend/src/models/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ const productSchema = new Schema({
type: String,
required: true,
},
isMarkedSold: {
type: Boolean,
required: true,
default: false,
tags: {
type: [String],
enum: ['Electronics', 'School Supplies', 'Dorm Essentials', 'Furniture', 'Clothes', 'Miscellaneous'],
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/EditProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function EditProduct() {
images: string[];
userEmail: string;
description: string;
isMarkedSold: boolean;
year: number;
category: string;
condition: string;
Expand Down Expand Up @@ -114,6 +115,7 @@ export function EditProduct() {
body.append("category", productCategory.current.value);
body.append("condition", productCondition.current.value);
body.append("userEmail", user.email || "");
body.append("isMarkedSold", String(product?.isMarkedSold));

// append existing image URLs
existingImages.forEach((url) => body.append("existingImages", url));
Expand Down
58 changes: 56 additions & 2 deletions frontend/src/pages/Individual-product-page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { faPenToSquare } from "@fortawesome/free-solid-svg-icons";
import { faPenToSquare, faCheck, faArrowUp } from "@fortawesome/free-solid-svg-icons";
import { faHeart as faHeartSolid } from "@fortawesome/free-solid-svg-icons";
import { faHeart as faHeartRegular } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useContext, useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { get, post } from "src/api/requests";
import { get, patch, post } from "src/api/requests";
import { FirebaseContext } from "src/utils/FirebaseProvider";
import EmblaCarousel from "src/components/EmblaCarousel";
import { EmblaOptionsType } from "embla-carousel";
Expand All @@ -20,6 +20,7 @@ export function IndividualProductPage() {
images: string[];
userEmail: string;
description: string;
isMarkedSold: boolean;
tags: string[];
}>();
const [error, setError] = useState<string>();
Expand Down Expand Up @@ -123,6 +124,33 @@ export function IndividualProductPage() {
setIsSubmitting(false);
}
};

const handleMarkSold = async () => {
if (!product) return;
const body = new FormData();
body.append("name", product.name);
body.append("price", product.price.toString());
body.append("description", product.description);
body.append("userEmail", product.userEmail);
product.images.forEach((url) => body.append("existingImages", url));
body.append("isMarkedSold", String(!product.isMarkedSold));

await patch(`/api/products/${id}`, body)
.then(async (res) => {
const response = await res.json();
if (res.ok) {
setProduct(response.updatedProduct);
navigate(`/products/${id}`);
} else {
alert("Failed to update product");
console.log(response);
}
})
.catch((e) => {
console.log(e);
});
};

const isCooling = Boolean(cooldownEnd && Date.now() < cooldownEnd);
// const secondsLeft = isCooling ? Math.ceil((cooldownEnd! - Date.now()) / 1000) : 0;
const msLeft = isCooling ? cooldownEnd! - Date.now() : 0;
Expand Down Expand Up @@ -235,9 +263,35 @@ export function IndividualProductPage() {

<hr className="my-6 w-full mx-auto h-0 border-[1px] border-solid border-gray-300" />

{hasPermissions &&
(product?.isMarkedSold ? (
<button
className="text-lg font-inter py-4 mb-6 font-bold border border-ucsd-blue text-ucsd-blue rounded-md"
onClick={handleMarkSold}
>
Renew on Marketplace <FontAwesomeIcon icon={faArrowUp} />
</button>
) : (
<button
className="text-lg font-inter py-4 mb-6 font-bold bg-ucsd-blue hover:bg-ucsd-darkblue text-white rounded-md transition-colors"
onClick={handleMarkSold}
>
Mark as Sold <FontAwesomeIcon icon={faCheck} />
</button>
))}

<h2 className="font-inter text-[#35393C] text-base md:text-xl font-normal pb-6">
USD ${product?.price?.toFixed(2)}
</h2>
{product?.isMarkedSold && (
<div className="bg-red-100 p-5 mb-6">
<p className="font-inter text-black text-base md:text-xl font-normal break-words">
{hasPermissions
? "This product has been marked as sold. It will not appear on the marketplace, but others can still be find it under your profile."
: "This product is no longer available."}
</p>
</div>
)}
{product?.description && (
<div className="bg-[#F5F0E6] p-5 mb-6">
<p className="font-inter text-black text-base md:text-xl font-normal break-words">
Expand Down