diff --git a/backend/src/controllers/products.ts b/backend/src/controllers/products.ts index d06153c..8ee8be3 100644 --- a/backend/src/controllers/products.ts +++ b/backend/src/controllers/products.ts @@ -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 @@ -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" }); } @@ -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 }, ); diff --git a/backend/src/models/product.ts b/backend/src/models/product.ts index 7ce1398..c92a7cd 100644 --- a/backend/src/models/product.ts +++ b/backend/src/models/product.ts @@ -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'], diff --git a/frontend/src/pages/EditProduct.tsx b/frontend/src/pages/EditProduct.tsx index 83205f9..cffd2c2 100644 --- a/frontend/src/pages/EditProduct.tsx +++ b/frontend/src/pages/EditProduct.tsx @@ -14,6 +14,7 @@ export function EditProduct() { images: string[]; userEmail: string; description: string; + isMarkedSold: boolean; year: number; category: string; condition: string; @@ -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)); diff --git a/frontend/src/pages/Individual-product-page.tsx b/frontend/src/pages/Individual-product-page.tsx index a36e59d..53e430d 100644 --- a/frontend/src/pages/Individual-product-page.tsx +++ b/frontend/src/pages/Individual-product-page.tsx @@ -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"; @@ -20,6 +20,7 @@ export function IndividualProductPage() { images: string[]; userEmail: string; description: string; + isMarkedSold: boolean; tags: string[]; }>(); const [error, setError] = useState(); @@ -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; @@ -235,9 +263,35 @@ export function IndividualProductPage() {
+ {hasPermissions && + (product?.isMarkedSold ? ( + + ) : ( + + ))} +

USD ${product?.price?.toFixed(2)}

+ {product?.isMarkedSold && ( +
+

+ {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."} +

+
+ )} {product?.description && (