diff --git a/backend/src/controllers/products.ts b/backend/src/controllers/products.ts index a100060..11a167c 100644 --- a/backend/src/controllers/products.ts +++ b/backend/src/controllers/products.ts @@ -131,7 +131,7 @@ export const deleteProductById = async (req: AuthenticatedRequest, res: Response if (!user) { return res.status(404).json({ message: "User not found" }); } - if (!user.productList.includes(id)) { + if (!user.productList.includes(new mongoose.Types.ObjectId(id))) { return res.status(400).json({ message: "User does not own this product" }); } @@ -164,7 +164,7 @@ export const updateProductById = [ return res.status(404).json({ message: "User not found" }); } - if (!user.productList.includes(id)) { + if (!user.productList.includes(new mongoose.Types.ObjectId(id))) { return res.status(400).json({ message: "User does not own this product" }); } @@ -191,6 +191,7 @@ export const updateProductById = [ description: req.body.description, images: finalImages, timeUpdated: new Date(), + isSold: req.body.isSold, }, { new: true }, ); diff --git a/backend/src/controllers/users.ts b/backend/src/controllers/users.ts index 7ddbf5b..1e14c01 100644 --- a/backend/src/controllers/users.ts +++ b/backend/src/controllers/users.ts @@ -1,6 +1,15 @@ import { Request, Response } from "express"; import UserModel from "src/models/user"; import { getAuth } from "firebase-admin/auth"; +import multer from "multer"; +import { v4 as uuidv4 } from "uuid"; +import { bucket } from "src/config/firebase"; +import { initializeApp } from "firebase/app"; +import { firebaseConfig } from "src/config/firebaseConfig"; +import { getStorage, ref, getDownloadURL } from "firebase/storage"; +import user from "src/models/user"; + +const upload = multer({ storage: multer.memoryStorage() }); export const getUsers = async (req: Request, res: Response) => { try { @@ -16,7 +25,7 @@ export const getUserById = async (req: Request, res: Response) => { try { const firebaseUid = req.params.firebaseUid; - const user = await UserModel.findOne({ firebaseUid: firebaseUid }); + const user = await UserModel.findOne({ firebaseUid: firebaseUid }).populate("productList"); if (!user) { return res.status(404).json({ message: "User not found" }); @@ -94,37 +103,74 @@ export const deleteUserById = async (req: Request, res: Response) => { }; // id: firebase user id -export const updateUserById = async (req: Request, res: Response) => { - try { - const { displayName, deactivateAccount } = req.body; - const firebaseUid = req.params.firebaseUid; - const updatedUser = await UserModel.findOne({ firebaseUid: firebaseUid }); - if (!updatedUser) { - throw new Error("User not found"); - } - - // Update fields if provided - if (displayName != undefined) { - updatedUser.displayName = displayName; - } - - if (deactivateAccount != undefined) { - updatedUser.activeUser = false; - } - - // Update Firebase user - const firebaseUser = await getAuth().getUserByEmail(updatedUser.userEmail); - if (firebaseUser) { - await getAuth().updateUser(firebaseUser.uid, { disabled: true }); +export const updateUserById = [ + upload.single("profilePic"), + async (req: Request, res: Response) => { + try { + const { displayName, deactivateAccount, biography } = req.body; + const firebaseUid = req.params.firebaseUid; + const updatedUser = await UserModel.findOne({ firebaseUid: firebaseUid }); + if (!updatedUser) { + throw new Error("User not found"); + } + + let newProfilePic; + + // Handle optional image uplaod + if (req.file) { + const fileName = `${uuidv4()}-${req.file.originalname}`; + const file = bucket.file(fileName); + + await file.save(req.file.buffer, { + metadata: { contentType: req.file.mimetype }, + }); + + const app = initializeApp(firebaseConfig); + const storage = getStorage(app); + newProfilePic = await getDownloadURL(ref(storage, fileName)); + } + + // Update fields if provided + if (displayName !== undefined) { + updatedUser.displayName = displayName; + } + + if (deactivateAccount !== undefined) { + updatedUser.activeUser = false; + } + + if (biography !== undefined) { + updatedUser.biography = biography; + } + + if (newProfilePic) { + updatedUser.profilePic = newProfilePic; + } + + // Update Firebase user + const firebaseUser = await getAuth().getUserByEmail(updatedUser.userEmail); + if (deactivateAccount) { + if (firebaseUser) { + await getAuth().updateUser(firebaseUser.uid, { disabled: true }); + } + } else { + if (firebaseUser) { + await getAuth().updateUser(firebaseUser.uid, { disabled: false }); + } + } + + await updatedUser.save(); + + res.status(200).json({ + message: "User successfully updated", + updatedUser, + }); + } catch (error) { + console.error(error); + res.status(500).json({ + message: "Error updating user", + error: error instanceof Error ? error.message : error, + }); } - - await updatedUser.save(); - - res.status(200).json({ - message: "User successfully updated", - updatedUser, - }); - } catch (error) { - res.status(500).json({ message: "Error updating user", error }); - } -}; + }, +]; diff --git a/backend/src/models/product.ts b/backend/src/models/product.ts index ca7386a..1d4df9a 100644 --- a/backend/src/models/product.ts +++ b/backend/src/models/product.ts @@ -25,6 +25,10 @@ const productSchema = new Schema({ required: true, }, images: [{ type: String }], + isSold: { + type: Boolean, + default: false, + } }); export type Product = HydratedDocument>; diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index a13b490..a913893 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -16,11 +16,12 @@ const userSchema = new Schema({ type: Date, required: true, }, - productList: { - type: [String], - required: true, - default: [], - }, + productList: [ + { + type: Schema.Types.ObjectId, + ref: "Product", + }, + ], savedProducts: { type: [String], required: true, @@ -30,6 +31,12 @@ const userSchema = new Schema({ type: String, required: true, }, + profilePic: { + type: String, + }, + biography: { + type: String, + }, }); export type User = HydratedDocument>; diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts index cf5b830..bb3c8a4 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/user.ts @@ -15,6 +15,6 @@ router.get("/:firebaseUid", getUserById); router.post("/", addUser); router.post("/:userId/saved-products", toggleSavedProduct); router.delete("/:id", deleteUserById); -router.patch("/:id", updateUserById); +router.patch("/:firebaseUid", updateUserById); export default router; diff --git a/frontend/public/profile-pic-default.png b/frontend/public/profile-pic-default.png new file mode 100644 index 0000000..7a12fef Binary files /dev/null and b/frontend/public/profile-pic-default.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2a21084..5524da6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,8 @@ import { AddProduct } from "../src/pages/AddProduct"; import { EditProduct } from "../src/pages/EditProduct"; import { IndividualProductPage } from "../src/pages/Individual-product-page"; import { PageNotFound } from "../src/pages/PageNotFound"; +import { ProfilePage } from "../src/pages/ProfilePage"; +import { EditProfile } from "../src/pages/EditProfile"; import FirebaseProvider from "../src/utils/FirebaseProvider"; import { SavedProducts } from "./pages/SavedProducts"; @@ -62,6 +64,22 @@ const router = createBrowserRouter([ path: "*", element: , }, + { + path: "/profile/:id", + element: ( + + + + ), + }, + { + path: "/edit-profile", + element: ( + + + + ), + }, ]); export default function App() { diff --git a/frontend/src/components/BackButton.tsx b/frontend/src/components/BackButton.tsx new file mode 100644 index 0000000..6b2fa6b --- /dev/null +++ b/frontend/src/components/BackButton.tsx @@ -0,0 +1,21 @@ +import { useNavigate } from "react-router-dom"; + +function BackButton() { + const navigate = useNavigate(); + + const handleBack = () => { + if (window.history.length > 1) { + navigate(-1); + } else { + navigate("/products"); + } + }; + + return ( + + ); +} + +export default BackButton; diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index b35b3c6..dee197c 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,4 +1,11 @@ -import { faBars, faCartShopping, faUser, faXmark, faHeart } from "@fortawesome/free-solid-svg-icons"; +import { + faBars, + faCartShopping, + faUser, + faXmark, + faDoorOpen, + faHeart, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useContext, useEffect, useRef, useState } from "react"; import { FirebaseContext } from "src/utils/FirebaseProvider"; @@ -71,21 +78,34 @@ export function Navbar() { onClick={() => (window.location.href = "/saved-products")} className="font-inter px-4 py-1 bg-transparent border-transparent rounded hover:bg-ucsd-darkblue transition-colors" > - + Saved
  • {user ? ( + ) : null} +
  • +
  • + {user ? ( + ) : ( @@ -93,7 +113,11 @@ export function Navbar() { onClick={openGoogleAuthentication} className="font-inter px-4 py-1 bg-transparent border-transparent rounded hover:bg-ucsd-darkblue transition-colors" > - + Sign In )} @@ -119,7 +143,7 @@ export function Navbar() {
  • +
  • + +
  • +
  • + {user ? ( + + ) : null} +
  • {user ? ( ) : ( @@ -143,7 +194,11 @@ export function Navbar() { onClick={openGoogleAuthentication} className="font-inter w-full text-center px-4 py-2 bg-transparent border-transparent rounded hover:bg-ucsd-darkblue transition-colors" > - + Sign In )} diff --git a/frontend/src/components/Product.tsx b/frontend/src/components/Product.tsx index 4a5b4b5..344193f 100644 --- a/frontend/src/components/Product.tsx +++ b/frontend/src/components/Product.tsx @@ -25,7 +25,9 @@ function Product({ const [isSaved, setIsSaved] = useState(initialIsSaved); const [isHovered, setIsHovered] = useState(false); const images = - productImages.length > 0 ? productImages : ["/productImages/product-placeholder.webp"]; + Array.isArray(productImages) && productImages.length > 0 + ? productImages + : ["/productImages/product-placeholder.webp"]; const toggleSave = async (e: React.MouseEvent) => { e.preventDefault(); diff --git a/frontend/src/pages/EditProfile.tsx b/frontend/src/pages/EditProfile.tsx new file mode 100644 index 0000000..55d8a1e --- /dev/null +++ b/frontend/src/pages/EditProfile.tsx @@ -0,0 +1,159 @@ +import { useState, useRef, useContext, useEffect, FormEvent } from "react"; +import { FirebaseContext } from "src/utils/FirebaseProvider"; +import { useNavigate } from "react-router-dom"; +import { get, patch } from "src/api/requests"; +import { Helmet } from "react-helmet-async"; + +interface UserProfile { + uid: string; + displayName?: string; + profilePic?: string; + biography?: string; +} + +export function EditProfile() { + const [profile, setProfile] = useState(null); + + const profileDisplayName = useRef(null); + const profileBiography = useRef(null); + const profilePicture = useRef(null); + const [currentProfilePic, setCurrentProfilePic] = useState(null); + + const [error, setError] = useState(false); + + const { user } = useContext(FirebaseContext); + // const { id } = useParams(); + const uid = user?.uid; + const navigate = useNavigate(); + + useEffect(() => { + const fetchUser = async () => { + if (!uid) return; + try { + const res = await get(`/api/users/${uid}`); + const profileData = await res.json(); + setProfile(profileData); + setCurrentProfilePic(profileData.profilePic); + } catch { + setError(true); + } + }; + fetchUser(); + }, [uid]); + + const handleEdit = async (e: FormEvent) => { + e.preventDefault(); + try { + if (profileDisplayName.current && profileBiography.current && user) { + let profilePic; + if (profilePicture.current && profilePicture.current.files) { + profilePic = profilePicture.current.files[0]; + } + + const body = new FormData(); + body.append("displayName", profileDisplayName.current.value); + body.append("biography", profileBiography.current.value); + if (profilePic) body.append("profilePic", profilePic); + + const res = await patch(`/api/users/${uid}`, body); + + if (!res.ok) { + throw new Error("Patch failed"); + } + + setError(false); + navigate(`/user-profile/${uid}`); + } else throw Error(); + } catch (err) { + setError(true); + } + }; + + const handleImageChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + setCurrentProfilePic(URL.createObjectURL(file)); + } + }; + + return ( + <> + + Low-Price Center Marketplace + +
    +

    Edit Profile

    +
    +
    + {/* Profile Display Name */} +
    + + +
    + {/* Biography */} +
    + +