From 81cd867a5f8c42bd760710527ffa15aae5727aef Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 00:10:55 -0500 Subject: [PATCH 01/16] test --- .../containers/User/Profile/ProfileUpdate.tsx | 454 +++++++++--------- server.js | 452 ----------------- 2 files changed, 238 insertions(+), 668 deletions(-) diff --git a/client/src/containers/User/Profile/ProfileUpdate.tsx b/client/src/containers/User/Profile/ProfileUpdate.tsx index 9c7e5ce..bda31f6 100644 --- a/client/src/containers/User/Profile/ProfileUpdate.tsx +++ b/client/src/containers/User/Profile/ProfileUpdate.tsx @@ -11,7 +11,6 @@ // type User = { // id: string; -// image: string; // email: string; // password?: string; // description: string; @@ -24,26 +23,29 @@ // phoneNumber: string; // driversLicense: string; // comments: string; +// qrCode?: string; // ✅ added QR support // }; // const ServerPort = // process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; -// // Cloudinary constants (replace with your Cloudinary cloud name & preset) -// const CLOUDINARY_UPLOAD_URL = "https://api.cloudinary.com/v1_1//image/upload"; -// const CLOUDINARY_UPLOAD_PRESET = ""; +// // ✅ Replace these with your actual Cloudinary credentials +// // const CLOUDINARY_UPLOAD_URL = `https://api.cloudinary.com/v1_1//image/upload`; +// // const CLOUDINARY_UPLOAD_PRESET = ""; +// const CLOUDINARY_UPLOAD_URL = `https://api.cloudinary.com/v1_1/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload`; +// const CLOUDINARY_UPLOAD_PRESET = process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET; // const ProfileUpdate: React.FC = () => { -// const userId = localStorage.userId; +// const userId = localStorage.getItem("userId"); // const [error, setError] = useState(null); // const [loading, setLoading] = useState(false); // const [successMessage, setSuccessMessage] = useState(null); // const [isSubmitting, setIsSubmitting] = useState(false); // const [imagePreview, setImagePreview] = useState(null); +// const [qrCodeImage, setQrCodeImage] = useState(null); // ✅ QR code state // const [formData, setFormData] = useState({ // id: "", -// image: "", // email: "", // password: "", // description: "", @@ -56,10 +58,12 @@ // phoneNumber: "", // driversLicense: "", // comments: "", +// qrCode: "", // }); // const [formErrors, setFormErrors] = useState>({}); +// // ✅ Load user profile // useEffect(() => { // const fetchUserData = async () => { // setLoading(true); @@ -69,10 +73,12 @@ // setLoading(false); // return; // } + // try { -// const response = await axios.get(`/api/user/view/${userId}`, { +// const response = await axios.get(`${ServerPort}/api/user/view/${userId}`, { // headers: { Authorization: `Bearer ${token}` }, // }); + // if (response.status === 200) { // const userData = response.data; // const availableFromDate = @@ -86,7 +92,9 @@ // password: "", // newPassword: "", // }); + // setImagePreview(userData.profileImage || null); +// setQrCodeImage(userData.qrCode || null); // } // } catch (err) { // handleApiError(err); @@ -98,16 +106,19 @@ // if (userId) fetchUserData(); // }, [userId]); +// // ✅ Upload image to Cloudinary // const handleImageUpload = async (file: File) => { // setLoading(true); // try { // const data = new FormData(); // data.append("file", file); -// data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET); +// data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!); // const res = await axios.post(CLOUDINARY_UPLOAD_URL, data); -// setFormData({ ...formData, profileImage: res.data.secure_url }); -// setImagePreview(res.data.secure_url); +// const imageUrl = res.data.secure_url; + +// setFormData({ ...formData, profileImage: imageUrl }); +// setImagePreview(imageUrl); // } catch (err) { // console.error("Upload error:", err); // setError("Image upload failed. Please try again."); @@ -116,11 +127,12 @@ // } // }; +// // ✅ Validate before sending update // const validateForm = () => { // const errors: any = {}; // if (!formData.email) errors.email = "Email is required"; // else if (!/\S+@\S+\.\S+/.test(formData.email)) -// errors.email = "Email is invalid"; +// errors.email = "Invalid email format"; // if (!formData.description) errors.description = "Description is required"; // if (!formData.userType) errors.userType = "User Type is required"; // if (!formData.experienceLevel) @@ -136,16 +148,19 @@ // return Object.keys(errors).length === 0; // }; +// // ✅ Submit update to backend // const handleUserUpdate = async (e: React.FormEvent) => { // e.preventDefault(); // if (!validateForm()) return; // setIsSubmitting(true); // setLoading(true); -// const token = localStorage.getItem("token"); // setError(null); // setSuccessMessage(null); +// const token = localStorage.getItem("token"); + +// // Remove password if not updating it // const updatedData: Partial = { ...formData }; // if (!formData.newPassword) delete updatedData.password; @@ -160,8 +175,10 @@ // }, // } // ); + // if (response.status === 200) { -// setSuccessMessage("Profile updated successfully 🎉"); +// setSuccessMessage("✅ Profile updated successfully!"); +// setQrCodeImage(response.data.qrCode || qrCodeImage); // ✅ refresh QR code if regenerated // setFormData({ ...formData, newPassword: "" }); // } // } catch (err) { @@ -172,9 +189,10 @@ // } // }; +// // ✅ Handle API errors // const handleApiError = (error: any) => { // if (axios.isAxiosError(error)) { -// setError(error.response?.data.message || error.message); +// setError(error.response?.data?.message || error.message); // } else { // setError("An unknown error occurred"); // } @@ -182,8 +200,8 @@ // return ( // -//

Update Your Profile

-// {loading && } +//

Update Your Profile

+// {loading && } // {error && {error}} // {successMessage && {successMessage}} @@ -195,7 +213,7 @@ // imagePreview || // "https://via.placeholder.com/150?text=Upload+Profile+Image" // } -// alt="Profile Preview" +// alt="Profile" // style={{ // width: 120, // height: 120, @@ -207,16 +225,28 @@ // { // const target = e.target as HTMLInputElement; // if (target.files?.[0]) { // handleImageUpload(target.files[0]); // } // }} -// className="mt-2" // /> // +// {/* ✅ QR Code Section */} +// {qrCodeImage && ( +//
+//
Your QR Code
+// QR Code +//
+// )} + // {/* Email + Description */} // // @@ -233,6 +263,7 @@ // {formErrors.email} // // + // // Description // // User Type // // setFormData({ ...formData, userType: e.target.value }) // } // isInvalid={!!formErrors.userType} -// > -// -// -// -// -// -// +// /> // // {formErrors.userType} // // + // // Experience Level // -// setFormData({ -// ...formData, -// experienceLevel: e.target.value, -// }) +// setFormData({ ...formData, experienceLevel: e.target.value }) // } // isInvalid={!!formErrors.experienceLevel} -// > -// -// -// -// -// +// /> // // {formErrors.experienceLevel} // @@ -311,6 +329,7 @@ // {formErrors.location} // // + // // Available From // // -// {/* Password + Phone */} +// {/* Phone + License */} // -// -// New Password -// -// setFormData({ ...formData, newPassword: e.target.value }) -// } -// isInvalid={!!formErrors.newPassword} -// /> -// -// {formErrors.newPassword} -// -// // // Phone Number // // -// -// {/* License + Comments */} -// // -// Drivers License +// Driver’s License // // -// -// Comments -// -// setFormData({ ...formData, comments: e.target.value }) -// } -// isInvalid={!!formErrors.comments} -// /> -// -// {formErrors.comments} -// -// // +// {/* Comments */} +// +// Comments +// +// setFormData({ ...formData, comments: e.target.value }) +// } +// isInvalid={!!formErrors.comments} +// /> +// +// {formErrors.comments} +// +// + +// {/* Password Update */} +// +// New Password +// +// setFormData({ ...formData, newPassword: e.target.value }) +// } +// /> +// + +// {/* Buttons */} //
// - - -
diff --git a/server.js b/server.js index db2315e..4297d84 100644 --- a/server.js +++ b/server.js @@ -1,365 +1,3 @@ -// // server.js -// require("dotenv").config(); -// const express = require("express"); -// const http = require("http"); -// const socketIo = require("socket.io"); -// const cors = require("cors"); -// const path = require("path"); -// const db = require("./models"); -// const axios = require("axios"); -// const bodyParser = require("body-parser"); -// const fetch = require("node-fetch"); - -// // Import controllers and socket handlers -// const handleVideoSocket = require("./config/videoSocket"); -// const handleMessageSocket = require("./config/messageSocket"); -// const loadController = require("./controllers/LoadController"); -// const driverController = require("./controllers/DriverController"); -// const messageRouter = require("./controllers/MessageController"); -// const LoadsRouter = require("./config/123LoadBoards/123LoadBoards"); - -// // Environment variables -// const { -// PORT = 3001, -// SOCKET_IO_SERVER_PORT, -// CLIENT_ID, -// CLIENT_SECRET, -// TOKEN_123, -// BEARER_123, -// URI_123, -// DEV_URI, -// USER_AGENT, -// } = process.env; - -// // Initialize express app and HTTP server -// const app = express(); -// const server = http.createServer(app); - -// // Middleware -// app.use(express.urlencoded({ extended: true })); -// app.use(express.json()); -// app.use(bodyParser.json()); -// app.use(express.static("client/build")); -// app.use(express.static("images")); - -// // CORS configuration for server and socket.io -// app.use( -// cors({ -// // origin: SOCKET_IO_SERVER_PORT, -// origin: "http://localhost:3000", -// credentials: true, -// methods: ["GET", "POST", "PUT", "DELETE"], -// }) -// ); - -// // Initialize socket.io with CORS configuration -// const io = socketIo(server, { -// cors: { -// // origin: SOCKET_IO_SERVER_PORT, -// origin: "http://localhost:3000", -// methods: ["GET", "POST"], -// }, -// }); - -// // Socket.io connections -// io.on("connection", (socket) => { -// handleVideoSocket(io, socket); -// handleMessageSocket(io, socket); -// }); - -// // 123Loads API route -// app.use("/api/123Loads", LoadsRouter); -// // Message API route -// app.use("/api/message", messageRouter); - -// // Other API routes -// app.use("/api/agreement", require("./controllers/AgreementController")); -// app.use("/api/user", require("./controllers/UserAPIRoutes")); -// app.use("/api/admin", require("./controllers/AdminController")); -// app.use("/api/newsletter", require("./controllers/NewsLetterController")); -// app.use("/api/it-help", require("./controllers/ITticketController")); -// app.use( -// "/api/employee-help", -// require("./controllers/EmployeeTicketController") -// ); -// app.use("/api/mail", require("./config/nodeMailer/nodeMailer")); -// app.use("/api/stripe", require("./config/stripe")); - -// app.use(require("./routes")); - -// // Load and driver routes -// app.get("/api/loads/user/:userId", loadController.getAllUserLoads); -// app.get("/api/drivers/user/:userId", driverController.getAllUserDrivers); -// app.get("/api/loads", loadController.getAllLoads); -// app.get("/api/drivers", driverController.getAllDrivers); -// app.post("/api/loads", loadController.createLoad); -// app.post("/api/drivers", driverController.createDriver); - -// // // API endpoint to handle the POST request -// // app.post("/api/load-search", async (req, res) => { -// // const { -// // originCity, -// // originState, -// // radius, -// // destinationType, -// // equipmentTypes, -// // minWeight, -// // maxMileage, -// // pickupDate, -// // companyRating, -// // modifiedStartDate, -// // modifiedEndDate, -// // } = req.body; - -// // try { -// // // Get the access token from the request headers or session (depending on your app) -// // const bearerToken = req.headers.authorization?.split(" ")[1]; - -// // if (!bearerToken) { -// // return res.status(400).send("Authorization token is missing"); -// // } - -// // // Structure the request body for the load search API -// // const requestBody = { -// // metadata: { -// // limit: 10, -// // sortBy: { field: "Origin", direction: "Ascending" }, -// // fields: "all", -// // type: "Regular", -// // }, -// // includeWithGreaterPickupDates: true, -// // origin: { -// // city: originCity, -// // states: [originState], -// // radius: radius, -// // type: "City", -// // }, -// // destination: { -// // type: destinationType, -// // }, -// // equipmentTypes: equipmentTypes, -// // minWeight: minWeight, -// // maxMileage: maxMileage, -// // pickupDates: [pickupDate], -// // company: { -// // minRating: companyRating, -// // }, -// // modifiedOnStart: modifiedStartDate, -// // modifiedOnEnd: modifiedEndDate, -// // }; - -// // // Send the structured request to the external API -// // const loadResp = await fetch( -// // "https://api.dev.123loadboard.com/loads/search", -// // { -// // method: "POST", -// // headers: { -// // "Content-Type": "application/json", -// // "123LB-Correlation-Id": "123GADZ", -// // "123LB-Api-Version": "1.3", -// // "User-Agent": USER_AGENT, -// // "123LB-AID": "Ba76be66d-dc2e-4045-87a3-adec3ae60eaf", -// // Authorization: `Bearer ${bearerToken}`, // Pass the bearer token -// // }, -// // body: JSON.stringify(requestBody), -// // } -// // ); - -// // const loadData = await loadResp.json(); - -// // if (loadResp.ok) { -// // return res.json(loadData); -// // } else { -// // console.error("Load Search API Error:", loadData); -// // return res.status(400).json({ error: "Failed to fetch load data" }); -// // } -// // } catch (error) { -// // console.error("Error during load search:", error); -// // res.status(500).json({ error: "An error occurred during load search" }); -// // } -// // }); - -// // // OAuth Flow - Authorization Route -// // app.get("/authorize", async (req, res) => { -// // const query = new URLSearchParams({ -// // response_type: "code", -// // client_id: CLIENT_ID, -// // redirect_uri: DEV_URI, -// // scope: "loadsearching", -// // state: "string", -// // login_hint: "gadzconnect_dev", -// // }).toString(); - -// // res.redirect(`${URI_123}/authorize?${query}`); -// // }); - -// // Route to handle token exchange and fetch loads -// // app.get("/auth/callback", async (req, res) => { -// // try { -// // const authCode = req.query.code; -// // console.log("Authorization Code:", authCode); - -// // // Exchange authorization code for access token -// // const formData = new URLSearchParams({ -// // grant_type: "authorization_code", -// // code: authCode, -// // client_id: CLIENT_ID, -// // redirect_uri: DEV_URI, -// // }).toString(); - -// // const tokenResp = await fetch(`${URI_123}/token`, { -// // method: "POST", -// // headers: { -// // "Content-Type": "application/x-www-form-urlencoded", -// // "123LB-Api-Version": "1.3", -// // "User-Agent": "gadzconnect_dev", -// // "123LB-AID": "Ba76be66d-dc2e-4045-87a3-adec3ae60eaf", -// // Authorization: -// // "Basic " + -// // Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64"), -// // }, -// // body: formData, -// // }); - -// // const tokenData = await tokenResp.json(); -// // console.log("Access Token Response:", tokenData); - -// // if (tokenData.access_token) { -// // const bearerToken = tokenData.access_token; - -// // // Use access token to fetch loads -// // const loadResp = await fetch(`${URI_123}/loads/search`, { -// // method: "POST", -// // headers: { -// // "123LB-Correlation-Id": "123GADZ", -// // "Content-Type": "application/json", -// // "123LB-Api-Version": "1.3", -// // "User-Agent": USER_AGENT, -// // "123LB-AID": "Ba76be66d-dc2e-4045-87a3-adec3ae60eaf", -// // Authorization: `Bearer ${bearerToken}`, -// // }, -// // body: JSON.stringify({ -// // metadata: { -// // limit: 10, -// // sortBy: { field: "Origin", direction: "Ascending" }, -// // fields: "all", -// // type: "Regular", -// // }, -// // includeWithGreaterPickupDates: true, -// // origin: { -// // states: ["IL"], -// // city: "Chicago", -// // radius: 100, -// // type: "City", -// // }, -// // destination: { -// // type: "Anywhere", -// // }, -// // equipmentTypes: ["Van", "Flatbed", "Reefer"], -// // includeLoadsWithoutWeight: true, -// // includeLoadsWithoutLength: true, -// // }), -// // }); - -// // const loadData = await loadResp.json(); -// // console.log("Load Response:", loadData); -// // res.send(loadData); -// // } else { -// // console.error("Access token not found in response:", tokenData); -// // res.status(400).send("Failed to retrieve access token."); -// // } -// // } catch (error) { -// // console.error(error); -// // res.status(500).send("An error occurred during the process."); -// // } -// // }); -// // // Route to handle token exchange ONLY (no auto-search) -// // app.get("/auth/callback", async (req, res) => { -// // try { -// // const authCode = req.query.code; -// // console.log("Authorization Code:", authCode); - -// // const formData = new URLSearchParams({ -// // grant_type: "authorization_code", -// // code: authCode, -// // client_id: CLIENT_ID, -// // redirect_uri: DEV_URI, -// // }).toString(); - -// // const tokenResp = await fetch(`${URI_123}/token`, { -// // method: "POST", -// // headers: { -// // "Content-Type": "application/x-www-form-urlencoded", -// // "123LB-Api-Version": "1.3", -// // "User-Agent": USER_AGENT, -// // "123LB-AID": "Ba76be66d-dc2e-4045-87a3-adec3ae60eaf", -// // Authorization: -// // "Basic " + -// // Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64"), -// // }, -// // body: formData, -// // }); - -// // const tokenData = await tokenResp.json(); -// // console.log("Access Token Response:", tokenData); - -// // if (!tokenData.access_token) { -// // console.error("Access token missing in response:", tokenData); -// // return res.status(400).send("Failed to retrieve access token."); -// // } - -// // const bearerToken = tokenData.access_token; - -// // // ✅ Option 1: Send token back as a cookie (recommended) -// // res.cookie("lb_access_token", bearerToken, { -// // httpOnly: true, -// // secure: process.env.NODE_ENV === "production", -// // sameSite: "Lax", -// // maxAge: 3600 * 1000, // 1 hour -// // }); - -// // // ✅ Option 2: Also send token in response body (frontend can store it) -// // res.json({ -// // success: true, -// // message: "Authorization successful", -// // access_token: bearerToken, -// // }); - -// // } catch (error) { -// // console.error("Error in /auth/callback:", error); -// // res.status(500).json({ error: "An error occurred during token exchange" }); -// // } -// // }); - -// // Test routes -// app.get("/api/config", (req, res) => { -// res.json({ success: true }); -// }); - -// app.get("/apiFun", (req, res) => { -// res.send("API FUN"); -// }); - -// // Serve React app -// app.get("*", (req, res) => { -// res.sendFile(path.join(__dirname, "./client/build/index.html")); -// }); - -// // { alter: true } { force: true } -// // Database connection and server startup -// db.sequelize -// .sync({}) -// .then(() => { -// server.listen(PORT, () => { -// console.log(`Server running on http://localhost:${PORT}`); -// }); -// }) -// .catch((err) => { -// console.error("Error syncing database:", err.message); -// }); - - -// server.js require("dotenv").config(); const express = require("express"); const http = require("http"); @@ -523,96 +161,6 @@ app.get("/auth/callback", async (req, res) => { } }); -// // Route to handle token exchange and fetch loads (with frontend data) -// app.post("/auth/callback/test", async (req, res) => { -// try { -// const { code } = req.query; -// const searchData = req.body; // Data sent from the frontend -// console.log("Authorization Code:", code); -// console.log("Frontend Search Data:", searchData); - -// if (!code) { -// return res.status(400).send({ error: "Missing authorization code" }); -// } - -// // Exchange authorization code for access token -// const formData = new URLSearchParams({ -// grant_type: "authorization_code", -// code, -// client_id: CLIENT_ID, -// redirect_uri: DEV_URI, -// }).toString(); - -// const tokenResp = await fetch(`${URI_123}/token`, { -// method: "POST", -// headers: { -// "Content-Type": "application/x-www-form-urlencoded", -// "123LB-Api-Version": "1.3", -// "User-Agent": "gadzconnect_dev", -// "123LB-AID": LOADBOARD_AID, -// Authorization: -// "Basic " + -// Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64"), -// }, -// body: formData, -// }); - -// const tokenData = await tokenResp.json(); -// console.log("Access Token Response:", tokenData); - -// if (!tokenData.access_token) { -// console.error("Access token not found in response:", tokenData); -// return res.status(400).send({ error: "Failed to retrieve access token." }); -// } - -// const bearerToken = tokenData.access_token; - -// // Use access token to fetch loads, using frontend data dynamically -// const loadResp = await fetch(`${URI_123}/loads/search`, { -// method: "POST", -// headers: { -// "123LB-Correlation-Id": "123GADZ", -// "Content-Type": "application/json", -// "123LB-Api-Version": "1.3", -// "User-Agent": USER_AGENT, -// "123LB-AID": LOADBOARD_AID, -// Authorization: `Bearer ${bearerToken}`, -// }, -// body: JSON.stringify({ -// // Merge defaults with user-provided search data -// metadata: { -// limit: searchData?.limit || 10, -// sortBy: searchData?.sortBy || { field: "Origin", direction: "Ascending" }, -// fields: "all", -// type: "Regular", -// }, -// includeWithGreaterPickupDates: true, -// origin: { -// states: searchData?.origin?.states || ["IL"], -// city: searchData?.origin?.city || "Chicago", -// radius: searchData?.origin?.radius || 100, -// type: searchData?.origin?.type || "City", -// }, -// destination: searchData?.destination || { type: "Anywhere" }, -// equipmentTypes: -// searchData?.equipmentTypes?.length > 0 -// ? searchData.equipmentTypes -// : ["Van", "Flatbed", "Reefer"], -// includeLoadsWithoutWeight: !!searchData?.includeLoadsWithoutWeight, -// includeLoadsWithoutLength: !!searchData?.includeLoadsWithoutLength, -// }), -// }); - -// const loadData = await loadResp.json(); -// console.log("Load Response:", loadData); - -// res.send(loadData); -// } catch (error) { -// console.error("Error during /auth/callback:", error); -// res.status(500).send({ error: "An error occurred during the process." }); -// } -// }); - // Route to handle token exchange and fetch loads dynamically app.post("/auth/callMeBack", async (req, res) => { try { From 66ed943b2b48442783aa65ab7cc656e79a916c5d Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 00:58:10 -0500 Subject: [PATCH 02/16] image working, showing up. --- .../containers/User/Profile/ProfileUpdate.tsx | 773 ++++++++++++++---- 1 file changed, 620 insertions(+), 153 deletions(-) diff --git a/client/src/containers/User/Profile/ProfileUpdate.tsx b/client/src/containers/User/Profile/ProfileUpdate.tsx index bda31f6..d9bfc05 100644 --- a/client/src/containers/User/Profile/ProfileUpdate.tsx +++ b/client/src/containers/User/Profile/ProfileUpdate.tsx @@ -428,7 +428,442 @@ // export default ProfileUpdate; -import React, { useState, useEffect } from "react"; +// import React, { useState, useEffect } from "react"; +// import axios from "axios"; +// import { +// Button, +// Col, +// Form, +// Row, +// Alert, +// Card, +// Spinner, +// } from "react-bootstrap"; + +// type User = { +// id: string; +// email: string; +// password?: string; +// description: string; +// userType: string; +// experienceLevel: string; +// location: string; +// availableFrom: string; +// newPassword?: string; +// profileImage?: string; +// phoneNumber: string; +// driversLicense: string; +// comments: string; +// qrCode?: string; +// }; + +// const ServerPort = +// process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; + +// const CLOUDINARY_UPLOAD_URL = `https://api.cloudinary.com/v1_1/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload`; +// const CLOUDINARY_UPLOAD_PRESET = process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET; + +// const ProfileUpdate: React.FC = () => { +// const userId = localStorage.getItem("userId"); + +// const [formData, setFormData] = useState({ +// id: "", +// email: "", +// password: "", +// description: "", +// userType: "", +// experienceLevel: "", +// location: "", +// availableFrom: "", +// newPassword: "", +// profileImage: "", +// phoneNumber: "", +// driversLicense: "", +// comments: "", +// qrCode: "", +// }); + +// const [imagePreview, setImagePreview] = useState(null); +// const [qrCodeImage, setQrCodeImage] = useState(null); +// const [formErrors, setFormErrors] = useState>({}); +// const [loading, setLoading] = useState(false); +// const [isSubmitting, setIsSubmitting] = useState(false); +// const [error, setError] = useState(null); +// const [successMessage, setSuccessMessage] = useState(null); + +// // ------------------------- +// // LOAD USER PROFILE +// // ------------------------- +// useEffect(() => { +// const fetchUserData = async () => { +// setLoading(true); + +// try { +// const token = localStorage.getItem("token"); +// if (!token) return setError("No token found."); + +// const response = await axios.get( +// `${ServerPort}/api/user/view/${userId}`, +// { headers: { Authorization: `Bearer ${token}` } } +// ); + +// const user = response.data; + +// setFormData({ +// ...user, +// availableFrom: user.availableFrom?.split("T")[0] || "", +// password: "", +// newPassword: "", +// }); + +// setImagePreview(user.profileImage || null); +// setQrCodeImage(user.qrCode || null); +// } catch (err) { +// handleApiError(err); +// } + +// setLoading(false); +// }; + +// if (userId) fetchUserData(); +// }, [userId]); + +// // ------------------------- +// // IMAGE UPLOAD TO CLOUDINARY +// // ------------------------- +// const handleImageUpload = async (file: File) => { +// setLoading(true); + +// try { +// const data = new FormData(); +// data.append("file", file); +// data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!); + +// const uploadRes = await axios.post(CLOUDINARY_UPLOAD_URL, data); +// const secureUrl = uploadRes.data.secure_url; + +// setFormData((prev) => ({ ...prev, profileImage: secureUrl })); +// setImagePreview(secureUrl); +// } catch (err) { +// setError("Image upload failed. Try again."); +// } + +// setLoading(false); +// }; + +// // ------------------------- +// // FORM VALIDATION +// // ------------------------- +// const validateForm = () => { +// const errors: Record = {}; +// const required = [ +// "email", +// "description", +// "userType", +// "experienceLevel", +// "location", +// "availableFrom", +// "phoneNumber", +// "driversLicense", +// "comments", +// ]; + +// required.forEach((field) => { +// // @ts-ignore +// if (!formData[field]) errors[field] = "This field is required"; +// }); + +// if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) +// errors.email = "Invalid email"; + +// setFormErrors(errors); + +// return Object.keys(errors).length === 0; +// }; + +// // ------------------------- +// // UPDATE USER +// // ------------------------- +// const handleUserUpdate = async (e: React.FormEvent) => { +// e.preventDefault(); +// if (!validateForm()) return; + +// setLoading(true); +// setIsSubmitting(true); +// setError(null); +// setSuccessMessage(null); + +// try { +// const token = localStorage.getItem("token"); + +// const payload = { ...formData }; +// if (!formData.newPassword) delete payload.password; + +// const res = await axios.put( +// `${ServerPort}/api/user/update/${userId}`, +// payload, +// { +// headers: { +// Authorization: `Bearer ${token}`, +// "Content-Type": "application/json", +// }, +// } +// ); + +// if (res.status === 200) { +// setSuccessMessage("Profile successfully updated!"); +// setQrCodeImage(res.data.qrCode || qrCodeImage); +// setFormData((prev) => ({ ...prev, newPassword: "" })); +// } +// } catch (err) { +// handleApiError(err); +// } + +// setIsSubmitting(false); +// setLoading(false); +// }; + +// // ------------------------- +// // ERROR HANDLER +// // ------------------------- +// const handleApiError = (error: any) => { +// if (axios.isAxiosError(error)) { +// setError(error.response?.data?.message || "Something went wrong."); +// } else { +// setError("An unexpected error occurred."); +// } +// }; + +// // ------------------------- +// // UI RENDER +// // ------------------------- +// return ( +// +//

Update Profile

+ +// {loading && ( +// +// )} +// {error && {error}} +// {successMessage && {successMessage}} + +//
+// {/* PROFILE IMAGE UPLOAD */} +//
+// Profile + +// ) => { +// const file = e.target.files?.[0]; +// if (file) handleImageUpload(file); +// }} +// /> +//
+ +// {/* QR CODE */} +// {qrCodeImage && ( +//
+//
Your QR Code
+// QR Code +//
+// )} + +// {/* FORM ROWS */} +// +// +// Email +// +// setFormData({ ...formData, email: e.target.value }) +// } +// /> +// +// {formErrors.email} +// +// + +// +// Description +// +// setFormData({ ...formData, description: e.target.value }) +// } +// /> +// +// {formErrors.description} +// +// +// + +// +// +// User Type +// +// setFormData({ ...formData, userType: e.target.value }) +// } +// /> +// +// {formErrors.userType} +// +// + +// +// Experience Level +// +// setFormData({ +// ...formData, +// experienceLevel: e.target.value, +// }) +// } +// /> +// +// {formErrors.experienceLevel} +// +// +// + +// +// +// Location +// +// setFormData({ ...formData, location: e.target.value }) +// } +// /> +// +// {formErrors.location} +// +// + +// +// Available From +// +// setFormData({ ...formData, availableFrom: e.target.value }) +// } +// /> +// +// {formErrors.availableFrom} +// +// +// + +// +// +// Phone Number +// +// setFormData({ ...formData, phoneNumber: e.target.value }) +// } +// /> +// +// {formErrors.phoneNumber} +// +// + +// +// Driver’s License +// +// setFormData({ +// ...formData, +// driversLicense: e.target.value, +// }) +// } +// /> +// +// {formErrors.driversLicense} +// +// +// + +// +// Comments +// +// setFormData({ ...formData, comments: e.target.value }) +// } +// /> +// +// {formErrors.comments} +// +// + +// +// New Password +// +// setFormData({ ...formData, newPassword: e.target.value }) +// } +// /> +// + +//
+// +//
+//
+//
+// ); +// }; + +// export default ProfileUpdate; + +import React, { useEffect, useState } from "react"; import axios from "axios"; import { Button, @@ -439,6 +874,7 @@ import { Card, Spinner, } from "react-bootstrap"; +import { Link } from "react-router-dom"; type User = { id: string; @@ -460,11 +896,8 @@ type User = { const ServerPort = process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; -const CLOUDINARY_UPLOAD_URL = `https://api.cloudinary.com/v1_1/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload`; -const CLOUDINARY_UPLOAD_PRESET = process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET; - const ProfileUpdate: React.FC = () => { - const userId = localStorage.getItem("userId"); + const userId = localStorage.getItem("userId") || ""; const [formData, setFormData] = useState({ id: "", @@ -483,6 +916,7 @@ const ProfileUpdate: React.FC = () => { qrCode: "", }); + const [selectedFile, setSelectedFile] = useState(null); const [imagePreview, setImagePreview] = useState(null); const [qrCodeImage, setQrCodeImage] = useState(null); const [formErrors, setFormErrors] = useState>({}); @@ -491,69 +925,71 @@ const ProfileUpdate: React.FC = () => { const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); - // ------------------------- - // LOAD USER PROFILE - // ------------------------- + // load user profile useEffect(() => { const fetchUserData = async () => { setLoading(true); - try { const token = localStorage.getItem("token"); - if (!token) return setError("No token found."); - - const response = await axios.get( - `${ServerPort}/api/user/view/${userId}`, - { headers: { Authorization: `Bearer ${token}` } } - ); - - const user = response.data; + if (!token) { + setError("No token found."); + setLoading(false); + return; + } - setFormData({ - ...user, - availableFrom: user.availableFrom?.split("T")[0] || "", - password: "", - newPassword: "", + const res = await axios.get(`${ServerPort}/api/user/view/${userId}`, { + headers: { Authorization: `Bearer ${token}` }, }); - setImagePreview(user.profileImage || null); - setQrCodeImage(user.qrCode || null); + if (res.status === 200) { + const user = res.data; + setFormData({ + ...user, + availableFrom: user.availableFrom?.split("T")[0] || "", + password: "", + newPassword: "", + }); + setImagePreview(user.profileImage || null); + setQrCodeImage(user.qrPNG || user.qrCode || null); + } else { + setError("Failed to fetch user data."); + } } catch (err) { handleApiError(err); + } finally { + setLoading(false); } - - setLoading(false); }; if (userId) fetchUserData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [userId]); - // ------------------------- - // IMAGE UPLOAD TO CLOUDINARY - // ------------------------- - const handleImageUpload = async (file: File) => { - setLoading(true); - - try { - const data = new FormData(); - data.append("file", file); - data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!); - - const uploadRes = await axios.post(CLOUDINARY_UPLOAD_URL, data); - const secureUrl = uploadRes.data.secure_url; - - setFormData((prev) => ({ ...prev, profileImage: secureUrl })); - setImagePreview(secureUrl); - } catch (err) { - setError("Image upload failed. Try again."); + // cleanup object URL when component unmounts / file changes + useEffect(() => { + return () => { + if (imagePreview && imagePreview.startsWith("blob:")) { + URL.revokeObjectURL(imagePreview); + } + }; + }, [imagePreview]); + + // handle native file input change (use plain input to avoid bootstrap issues) + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] ?? null; + if (file) { + // revoke previous blob url if any + if (imagePreview && imagePreview.startsWith("blob:")) { + URL.revokeObjectURL(imagePreview); + } + setSelectedFile(file); + setImagePreview(URL.createObjectURL(file)); + // don't mutate formData.profileImage here — backend will return permanent URL + } else { + setSelectedFile(null); } - - setLoading(false); }; - // ------------------------- - // FORM VALIDATION - // ------------------------- const validateForm = () => { const errors: Record = {}; const required = [ @@ -566,77 +1002,124 @@ const ProfileUpdate: React.FC = () => { "phoneNumber", "driversLicense", "comments", - ]; + ] as const; - required.forEach((field) => { + for (const field of required) { // @ts-ignore if (!formData[field]) errors[field] = "This field is required"; - }); + } - if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) + if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) { errors.email = "Invalid email"; + } setFormErrors(errors); - return Object.keys(errors).length === 0; }; - // ------------------------- - // UPDATE USER - // ------------------------- + // Submit: if selectedFile exists -> send multipart/form-data to backend, + // otherwise send JSON. Backend should accept multipart and handle saving to Cloudinary. const handleUserUpdate = async (e: React.FormEvent) => { e.preventDefault(); + setError(null); + setSuccessMessage(null); + if (!validateForm()) return; setLoading(true); setIsSubmitting(true); - setError(null); - setSuccessMessage(null); try { const token = localStorage.getItem("token"); + if (!token) throw new Error("No token - please sign in."); + + if (selectedFile) { + // send multipart to backend; backend must use multer to accept `profileImage` + const form = new FormData(); + + // append scalar fields + Object.entries(formData).forEach(([k, v]) => { + if (v !== undefined && v !== null) form.append(k, String(v)); + }); - const payload = { ...formData }; - if (!formData.newPassword) delete payload.password; - - const res = await axios.put( - `${ServerPort}/api/user/update/${userId}`, - payload, - { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, + form.append("profileImage", selectedFile); + + const res = await axios.put( + `${ServerPort}/api/user/update/${userId}`, + form, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "multipart/form-data", + }, + } + ); + + if (res.status === 200) { + const updatedUser = res.data.user ?? res.data; + setFormData((prev) => ({ + ...prev, + ...updatedUser, + newPassword: "", + })); + // server should return permanent image URL (profileImage) + setImagePreview(updatedUser.profileImage || imagePreview); + setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); + setSuccessMessage("Profile updated and image uploaded."); + setSelectedFile(null); + } else { + setError("Unexpected server response when uploading image."); } - ); + } else { + // no file selected — send JSON update + const payload: Partial = { ...formData }; + if (!formData.newPassword) delete payload.password; + + const res = await axios.put( + `${ServerPort}/api/user/update/${userId}`, + payload, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); - if (res.status === 200) { - setSuccessMessage("Profile successfully updated!"); - setQrCodeImage(res.data.qrCode || qrCodeImage); - setFormData((prev) => ({ ...prev, newPassword: "" })); + if (res.status === 200) { + const updatedUser = res.data.user ?? res.data; + setFormData((prev) => ({ + ...prev, + ...updatedUser, + newPassword: "", + })); + setImagePreview(updatedUser.profileImage || imagePreview); + setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); + setSuccessMessage("Profile updated."); + } else { + setError("Unexpected server response when updating profile."); + } } } catch (err) { handleApiError(err); + } finally { + setIsSubmitting(false); + setLoading(false); } - - setIsSubmitting(false); - setLoading(false); }; - // ------------------------- - // ERROR HANDLER - // ------------------------- - const handleApiError = (error: any) => { - if (axios.isAxiosError(error)) { - setError(error.response?.data?.message || "Something went wrong."); + const handleApiError = (err: any) => { + if (axios.isAxiosError(err)) { + const msg = + (err.response && (err.response.data?.message || err.response.data)) || + err.message || + "Server error"; + setError(String(msg)); } else { - setError("An unexpected error occurred."); + setError(String(err || "Unknown error")); } }; - // ------------------------- - // UI RENDER - // ------------------------- return (

Update Profile

@@ -647,12 +1130,13 @@ const ProfileUpdate: React.FC = () => { {error && {error}} {successMessage && {successMessage}} -
- {/* PROFILE IMAGE UPLOAD */} + + {/* IMAGE PREVIEW + NATIVE FILE INPUT (avoid Form.Control for file) */}
Profile { border: "3px solid #ddd", }} /> - - ) => { - const file = e.target.files?.[0]; - if (file) handleImageUpload(file); - }} - /> +
+ + {selectedFile && ( +
+ Selected: {selectedFile.name} +
+ )} +
{/* QR CODE */} {qrCodeImage && (
Your QR Code
- QR Code + QR Code
)} - {/* FORM ROWS */} + {/* FORM FIELDS */} - + Email - setFormData({ ...formData, email: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, email: e.target.value })} /> {formErrors.email} - + Description - setFormData({ ...formData, description: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, description: e.target.value })} /> {formErrors.description} @@ -718,32 +1207,27 @@ const ProfileUpdate: React.FC = () => { - + User Type - setFormData({ ...formData, userType: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, userType: e.target.value })} /> {formErrors.userType} - + Experience Level - setFormData({ - ...formData, - experienceLevel: e.target.value, - }) + setFormData({ ...formData, experienceLevel: e.target.value }) } /> @@ -753,30 +1237,26 @@ const ProfileUpdate: React.FC = () => { - + Location - setFormData({ ...formData, location: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, location: e.target.value })} /> {formErrors.location} - + Available From - setFormData({ ...formData, availableFrom: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, availableFrom: e.target.value })} /> {formErrors.availableFrom} @@ -785,33 +1265,26 @@ const ProfileUpdate: React.FC = () => { - + Phone Number - setFormData({ ...formData, phoneNumber: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, phoneNumber: e.target.value })} /> {formErrors.phoneNumber} - + Driver’s License - setFormData({ - ...formData, - driversLicense: e.target.value, - }) - } + onChange={(e) => setFormData({ ...formData, driversLicense: e.target.value })} /> {formErrors.driversLicense} @@ -819,42 +1292,36 @@ const ProfileUpdate: React.FC = () => { - + Comments - setFormData({ ...formData, comments: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, comments: e.target.value })} /> {formErrors.comments} - + New Password - setFormData({ ...formData, newPassword: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, newPassword: e.target.value })} /> -
- + + +
From 14769c5b3e8f4b0f68fbbbe4edcc4947168ece54 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 01:38:21 -0500 Subject: [PATCH 03/16] Testing to see if this will work --- .../User/Profile/ProfileUpdate.module.css | 112 ++++++++++++++++++ controllers/UserAPIRoutes.js | 77 +++++++++++- 2 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 client/src/containers/User/Profile/ProfileUpdate.module.css diff --git a/client/src/containers/User/Profile/ProfileUpdate.module.css b/client/src/containers/User/Profile/ProfileUpdate.module.css new file mode 100644 index 0000000..b66a704 --- /dev/null +++ b/client/src/containers/User/Profile/ProfileUpdate.module.css @@ -0,0 +1,112 @@ +.container { + max-width: 650px; + margin: 2rem auto; + padding: 2rem; + background: #ffffff; + border-radius: 1rem; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.title { + text-align: center; + margin-bottom: 2rem; + color: #222; + font-size: 1.8rem; + font-weight: 600; +} + +.form label { + display: block; + font-weight: 600; + margin-bottom: 0.4rem; + color: #444; +} + +.section { + margin-bottom: 1.5rem; +} + +.section input, +.section textarea { + width: 100%; + padding: 0.7rem; + border-radius: 0.5rem; + border: 1px solid #ccc; + font-size: 1rem; + transition: 0.2s; +} + +.section input:focus, +.section textarea:focus { + border-color: #007bff; + outline: none; +} + +.preview { + display: block; + margin: 0.5rem auto 1rem; + width: 160px; + height: 160px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #eee; +} + +.placeholder { + width: 160px; + height: 160px; + border-radius: 50%; + background: #f3f3f3; + margin: 0.5rem auto 1rem; + display: flex; + justify-content: center; + align-items: center; + color: #777; + border: 2px solid #ddd; +} + +.qrSection { + text-align: center; + margin-top: 2rem; +} + +.qr { + width: 170px; + height: 170px; + margin-top: 0.7rem; + border-radius: 0.5rem; + border: 2px solid #ddd; +} + +.submit { + text-align: center; + margin-top: 1.5rem; +} + +.submit button { + background: #007bff; + color: white; + border: none; + padding: 0.9rem 2rem; + border-radius: 0.5rem; + cursor: pointer; + font-size: 1.1rem; + font-weight: 600; + transition: 0.2s; +} + +.submit button:hover { + background: #005fcc; +} + +.submit button:disabled { + background: #888; + cursor: not-allowed; +} + +.status { + text-align: center; + color: green; + margin-top: 1rem; + font-weight: 600; +} diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index 917170d..fa504b0 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -64,15 +64,15 @@ router.put("/update/:id", async (req, res) => { .send({ success: false, message: "User not found" }); } - // Prepare the update data - const updateData = { ...req.body }; + // Extract newPassword separately and keep the rest of the update fields + const { newPassword, ...rest } = req.body; + const updateData = { ...rest }; // Check if a new password has been provided - if (updateData.newPassword) { + if (newPassword) { // Hash the new password - const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); + const hashedPassword = await bcrypt.hash(newPassword, 10); updateData.password = hashedPassword; // Set the hashed password - delete updateData.newPassword; // Remove newPassword from the update data } // Update the user @@ -282,6 +282,73 @@ router.post("/login", (req, res) => { // res.status(500).send({ success: false, message: "Internal Server Error" }); // } // }); +// router.put("/user/update/:userId", upload.single("image"), async (req, res) => { +// try { +// const { newPassword } = req.body; + +// // 1️⃣ Fetch user +// const user = await db.User.findByPk(req.params.userId); +// if (!user) { +// return res.status(404).json({ success: false, message: "User not found" }); +// } + +// // 2️⃣ Handle password change +// if (newPassword) { +// const hashedPassword = await bcrypt.hash(newPassword, 10); +// req.body.password = hashedPassword; +// delete req.body.newPassword; +// } + +// // 3️⃣ Handle image upload if provided +// let imageUrl = user.image; // Keep old image if none uploaded +// if (req.file) { +// try { +// const result = await cloudinary.uploader.upload(req.file.path, { +// folder: "users", +// public_id: `user_${user.id}_${Date.now()}`, +// transformation: [{ width: 500, height: 500, crop: "fill" }], +// }); +// imageUrl = result.secure_url; +// } catch (uploadErr) { +// console.error("Cloudinary upload failed:", uploadErr); +// return res.status(500).json({ +// success: false, +// message: "Image upload failed", +// }); +// } +// } + +// // 4️⃣ Prepare update data +// const updateData = { ...req.body, image: imageUrl }; + +// // 5️⃣ Update user +// const [updatedRows] = await db.User.update(updateData, { +// where: { id: req.params.userId }, +// }); + +// if (updatedRows === 0) { +// return res +// .status(400) +// .json({ success: false, message: "No updates made" }); +// } + +// // 6️⃣ Fetch updated user +// const updatedUser = await db.User.findByPk(req.params.userId); + +// res.status(200).json({ +// success: true, +// message: "User updated successfully", +// user: updatedUser, +// }); +// } catch (error) { +// console.error("Error updating user:", error); +// res.status(500).json({ +// success: false, +// message: "Internal Server Error", +// }); +// } +// }); + router.put("/user/update/:userId", upload.single("image"), async (req, res) => { try { const { newPassword } = req.body; From 6b66be8fef8c27be63b35ef35cfb39c3ce1aaeb3 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 01:45:57 -0500 Subject: [PATCH 04/16] backworking --- controllers/UserAPIRoutes.js | 77 +++--------------------------------- 1 file changed, 5 insertions(+), 72 deletions(-) diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index fa504b0..917170d 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -64,15 +64,15 @@ router.put("/update/:id", async (req, res) => { .send({ success: false, message: "User not found" }); } - // Extract newPassword separately and keep the rest of the update fields - const { newPassword, ...rest } = req.body; - const updateData = { ...rest }; + // Prepare the update data + const updateData = { ...req.body }; // Check if a new password has been provided - if (newPassword) { + if (updateData.newPassword) { // Hash the new password - const hashedPassword = await bcrypt.hash(newPassword, 10); + const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); updateData.password = hashedPassword; // Set the hashed password + delete updateData.newPassword; // Remove newPassword from the update data } // Update the user @@ -282,73 +282,6 @@ router.post("/login", (req, res) => { // res.status(500).send({ success: false, message: "Internal Server Error" }); // } // }); -// router.put("/user/update/:userId", upload.single("image"), async (req, res) => { -// try { -// const { newPassword } = req.body; - -// // 1️⃣ Fetch user -// const user = await db.User.findByPk(req.params.userId); -// if (!user) { -// return res.status(404).json({ success: false, message: "User not found" }); -// } - -// // 2️⃣ Handle password change -// if (newPassword) { -// const hashedPassword = await bcrypt.hash(newPassword, 10); -// req.body.password = hashedPassword; -// delete req.body.newPassword; -// } - -// // 3️⃣ Handle image upload if provided -// let imageUrl = user.image; // Keep old image if none uploaded -// if (req.file) { -// try { -// const result = await cloudinary.uploader.upload(req.file.path, { -// folder: "users", -// public_id: `user_${user.id}_${Date.now()}`, -// transformation: [{ width: 500, height: 500, crop: "fill" }], -// }); -// imageUrl = result.secure_url; -// } catch (uploadErr) { -// console.error("Cloudinary upload failed:", uploadErr); -// return res.status(500).json({ -// success: false, -// message: "Image upload failed", -// }); -// } -// } - -// // 4️⃣ Prepare update data -// const updateData = { ...req.body, image: imageUrl }; - -// // 5️⃣ Update user -// const [updatedRows] = await db.User.update(updateData, { -// where: { id: req.params.userId }, -// }); - -// if (updatedRows === 0) { -// return res -// .status(400) -// .json({ success: false, message: "No updates made" }); -// } - -// // 6️⃣ Fetch updated user -// const updatedUser = await db.User.findByPk(req.params.userId); - -// res.status(200).json({ -// success: true, -// message: "User updated successfully", -// user: updatedUser, -// }); -// } catch (error) { -// console.error("Error updating user:", error); -// res.status(500).json({ -// success: false, -// message: "Internal Server Error", -// }); -// } -// }); - router.put("/user/update/:userId", upload.single("image"), async (req, res) => { try { const { newPassword } = req.body; From a18ddaf30efef2938f39b711910cf082abec9341 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 01:56:17 -0500 Subject: [PATCH 05/16] testing --- controllers/UserAPIRoutes.js | 70 ++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index 917170d..b49ef0d 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -52,53 +52,68 @@ router.put("/update/:id", async (req, res) => { // console.log("Updating user..."); try { - // console.log("Request Body:", req.body); - - // Fetch the user using findByPk - const user = await db.User.findByPk(req.params.id); + const { newPassword } = req.body; + // 1️⃣ Fetch user + const user = await db.User.findByPk(req.params.userId); if (!user) { - // console.log("User not found"); - return res - .status(404) - .send({ success: false, message: "User not found" }); + return res.status(404).json({ success: false, message: "User not found" }); } - // Prepare the update data - const updateData = { ...req.body }; + // 2️⃣ Handle password change + if (newPassword) { + const hashedPassword = await bcrypt.hash(newPassword, 10); + req.body.password = hashedPassword; + delete req.body.newPassword; + } - // Check if a new password has been provided - if (updateData.newPassword) { - // Hash the new password - const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); - updateData.password = hashedPassword; // Set the hashed password - delete updateData.newPassword; // Remove newPassword from the update data + // 3️⃣ Handle image upload if provided + let imageUrl = user.image; // Keep old image if none uploaded + if (req.file) { + try { + const result = await cloudinary.uploader.upload(req.file.path, { + folder: "users", + public_id: `user_${user.id}_${Date.now()}`, + transformation: [{ width: 500, height: 500, crop: "fill" }], + }); + imageUrl = result.secure_url; + } catch (uploadErr) { + console.error("Cloudinary upload failed:", uploadErr); + return res.status(500).json({ + success: false, + message: "Image upload failed", + }); + } } - // Update the user + // 4️⃣ Prepare update data + const updateData = { ...req.body, image: imageUrl }; + + // 5️⃣ Update user const [updatedRows] = await db.User.update(updateData, { - where: { id: req.params.id }, + where: { id: req.params.userId }, }); if (updatedRows === 0) { - // console.log("No rows updated"); return res - .status(404) - .send({ success: false, message: "No updates made" }); + .status(400) + .json({ success: false, message: "No updates made" }); } - // Fetch the updated user to return - const updatedUser = await db.User.findByPk(req.params.id); + // 6️⃣ Fetch updated user + const updatedUser = await db.User.findByPk(req.params.userId); - // console.log(`User with ID ${req.params.id} updated successfully.`); - res.status(200).send({ + res.status(200).json({ success: true, message: "User updated successfully", - user: updatedUser, // Return updated user data + user: updatedUser, }); } catch (error) { console.error("Error updating user:", error); - res.status(500).send({ success: false, message: "Internal Server Error" }); + res.status(500).json({ + success: false, + message: "Internal Server Error", + }); } }); @@ -282,6 +297,7 @@ router.post("/login", (req, res) => { // res.status(500).send({ success: false, message: "Internal Server Error" }); // } // }); + router.put("/user/update/:userId", upload.single("image"), async (req, res) => { try { const { newPassword } = req.body; From 34d7e4ca0c68d72670e88179000bbb00a5bbe6c6 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 02:03:03 -0500 Subject: [PATCH 06/16] update --- controllers/UserAPIRoutes.js | 69 ++++++++++++++---------------------- 1 file changed, 27 insertions(+), 42 deletions(-) diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index b49ef0d..184f725 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -52,68 +52,53 @@ router.put("/update/:id", async (req, res) => { // console.log("Updating user..."); try { - const { newPassword } = req.body; + // console.log("Request Body:", req.body); + + // Fetch the user using findByPk + const user = await db.User.findByPk(req.params.id); - // 1️⃣ Fetch user - const user = await db.User.findByPk(req.params.userId); if (!user) { - return res.status(404).json({ success: false, message: "User not found" }); + // console.log("User not found"); + return res + .status(404) + .send({ success: false, message: "User not found" }); } - // 2️⃣ Handle password change - if (newPassword) { - const hashedPassword = await bcrypt.hash(newPassword, 10); - req.body.password = hashedPassword; - delete req.body.newPassword; - } + // Prepare the update data + const updateData = { ...req.body }; - // 3️⃣ Handle image upload if provided - let imageUrl = user.image; // Keep old image if none uploaded - if (req.file) { - try { - const result = await cloudinary.uploader.upload(req.file.path, { - folder: "users", - public_id: `user_${user.id}_${Date.now()}`, - transformation: [{ width: 500, height: 500, crop: "fill" }], - }); - imageUrl = result.secure_url; - } catch (uploadErr) { - console.error("Cloudinary upload failed:", uploadErr); - return res.status(500).json({ - success: false, - message: "Image upload failed", - }); - } + // Check if a new password has been provided + if (updateData.newPassword) { + // Hash the new password + const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); + updateData.password = hashedPassword; // Set the hashed password + delete updateData.newPassword; // Remove newPassword from the update data } - // 4️⃣ Prepare update data - const updateData = { ...req.body, image: imageUrl }; - - // 5️⃣ Update user + // Update the user const [updatedRows] = await db.User.update(updateData, { - where: { id: req.params.userId }, + where: { id: req.params.id }, }); if (updatedRows === 0) { + // console.log("No rows updated"); return res - .status(400) - .json({ success: false, message: "No updates made" }); + .status(404) + .send({ success: false, message: "No updates made" }); } - // 6️⃣ Fetch updated user - const updatedUser = await db.User.findByPk(req.params.userId); + // Fetch the updated user to return + const updatedUser = await db.User.findByPk(req.params.id); - res.status(200).json({ + // console.log(`User with ID ${req.params.id} updated successfully.`); + res.status(200).send({ success: true, message: "User updated successfully", - user: updatedUser, + user: updatedUser, // Return updated user data }); } catch (error) { console.error("Error updating user:", error); - res.status(500).json({ - success: false, - message: "Internal Server Error", - }); + res.status(500).send({ success: false, message: "Internal Server Error" }); } }); From b24bb00150a11c89ec19b2698e56513dc53d3f3c Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 02:13:44 -0500 Subject: [PATCH 07/16] testing --- controllers/UserAPIRoutes.js | 109 +++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 23 deletions(-) diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index 184f725..a71666d 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -48,54 +48,117 @@ router.get("/view/:id", (req, res) => { }); }); -router.put("/update/:id", async (req, res) => { - // console.log("Updating user..."); +// router.put("/update/:id", async (req, res) => { +// // console.log("Updating user..."); - try { - // console.log("Request Body:", req.body); +// try { +// // console.log("Request Body:", req.body); - // Fetch the user using findByPk - const user = await db.User.findByPk(req.params.id); +// // Fetch the user using findByPk +// const user = await db.User.findByPk(req.params.id); + +// if (!user) { +// // console.log("User not found"); +// return res +// .status(404) +// .send({ success: false, message: "User not found" }); +// } + +// // Prepare the update data +// const updateData = { ...req.body }; + +// // Check if a new password has been provided +// if (updateData.newPassword) { +// // Hash the new password +// const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); +// updateData.password = hashedPassword; // Set the hashed password +// delete updateData.newPassword; // Remove newPassword from the update data +// } +// // Update the user +// const [updatedRows] = await db.User.update(updateData, { +// where: { id: req.params.id }, +// }); + +// if (updatedRows === 0) { +// // console.log("No rows updated"); +// return res +// .status(404) +// .send({ success: false, message: "No updates made" }); +// } + +// // Fetch the updated user to return +// const updatedUser = await db.User.findByPk(req.params.id); + +// // console.log(`User with ID ${req.params.id} updated successfully.`); +// res.status(200).send({ +// success: true, +// message: "User updated successfully", +// user: updatedUser, // Return updated user data +// }); +// } catch (error) { +// console.error("Error updating user:", error); +// res.status(500).send({ success: false, message: "Internal Server Error" }); +// } +// }); +router.put("/update/:id", upload.single("image"), async (req, res) => { + try { + // 1️⃣ Fetch the user + const user = await db.User.findByPk(req.params.id); if (!user) { - // console.log("User not found"); - return res - .status(404) - .send({ success: false, message: "User not found" }); + return res.status(404).send({ success: false, message: "User not found" }); } - // Prepare the update data + // 2️⃣ Handle password update const updateData = { ...req.body }; - // Check if a new password has been provided if (updateData.newPassword) { - // Hash the new password const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); - updateData.password = hashedPassword; // Set the hashed password - delete updateData.newPassword; // Remove newPassword from the update data + updateData.password = hashedPassword; + delete updateData.newPassword; + } + + // 3️⃣ Handle image upload (optional) + let imageUrl = user.image; // keep previous image by default + + if (req.file) { + try { + const result = await cloudinary.uploader.upload(req.file.path, { + folder: "users", + public_id: `user_${user.id}_${Date.now()}`, + transformation: [{ width: 500, height: 500, crop: "fill" }], + }); + + imageUrl = result.secure_url; + } catch (err) { + console.error("Cloudinary Upload Error:", err); + return res.status(500).json({ + success: false, + message: "Image upload failed", + }); + } } - // Update the user + updateData.image = imageUrl; + + // 4️⃣ Update the user const [updatedRows] = await db.User.update(updateData, { where: { id: req.params.id }, }); if (updatedRows === 0) { - // console.log("No rows updated"); - return res - .status(404) - .send({ success: false, message: "No updates made" }); + return res.status(404).send({ success: false, message: "No updates made" }); } - // Fetch the updated user to return + // 5️⃣ Fetch updated user const updatedUser = await db.User.findByPk(req.params.id); - // console.log(`User with ID ${req.params.id} updated successfully.`); res.status(200).send({ success: true, message: "User updated successfully", - user: updatedUser, // Return updated user data + user: updatedUser, }); + } catch (error) { console.error("Error updating user:", error); res.status(500).send({ success: false, message: "Internal Server Error" }); From 0c4709bedda1f3282bafd284addd56f4137f0c86 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 02:40:03 -0500 Subject: [PATCH 08/16] testing --- controllers/UserAPIRoutes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index a71666d..6e66355 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -101,7 +101,7 @@ router.get("/view/:id", (req, res) => { // res.status(500).send({ success: false, message: "Internal Server Error" }); // } // }); -router.put("/update/:id", upload.single("image"), async (req, res) => { +router.put("/update/:id", upload.single("profileImage"), async (req, res) => { try { // 1️⃣ Fetch the user const user = await db.User.findByPk(req.params.id); From 736eae94aa4f2f29e416ffa63311f82270d8562f Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 02:58:07 -0500 Subject: [PATCH 09/16] test --- controllers/UserAPIRoutes.js | 181 ++++++++--------------------------- 1 file changed, 40 insertions(+), 141 deletions(-) diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index 6e66355..cb95002 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -48,59 +48,6 @@ router.get("/view/:id", (req, res) => { }); }); -// router.put("/update/:id", async (req, res) => { -// // console.log("Updating user..."); - -// try { -// // console.log("Request Body:", req.body); - -// // Fetch the user using findByPk -// const user = await db.User.findByPk(req.params.id); - -// if (!user) { -// // console.log("User not found"); -// return res -// .status(404) -// .send({ success: false, message: "User not found" }); -// } - -// // Prepare the update data -// const updateData = { ...req.body }; - -// // Check if a new password has been provided -// if (updateData.newPassword) { -// // Hash the new password -// const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); -// updateData.password = hashedPassword; // Set the hashed password -// delete updateData.newPassword; // Remove newPassword from the update data -// } - -// // Update the user -// const [updatedRows] = await db.User.update(updateData, { -// where: { id: req.params.id }, -// }); - -// if (updatedRows === 0) { -// // console.log("No rows updated"); -// return res -// .status(404) -// .send({ success: false, message: "No updates made" }); -// } - -// // Fetch the updated user to return -// const updatedUser = await db.User.findByPk(req.params.id); - -// // console.log(`User with ID ${req.params.id} updated successfully.`); -// res.status(200).send({ -// success: true, -// message: "User updated successfully", -// user: updatedUser, // Return updated user data -// }); -// } catch (error) { -// console.error("Error updating user:", error); -// res.status(500).send({ success: false, message: "Internal Server Error" }); -// } -// }); router.put("/update/:id", upload.single("profileImage"), async (req, res) => { try { // 1️⃣ Fetch the user @@ -298,119 +245,71 @@ router.post("/login", (req, res) => { }); }); -// router.put("/user/update/:userId", async (req, res) => { -// const { newPassword } = req.body; - +// router.put("/user/update/:userId", upload.single("image"), async (req, res) => { // try { -// // Fetch the user using findByPk -// const user = await db.User.findByPk(req.params.userId); +// const { newPassword } = req.body; +// // 1️⃣ Fetch user +// const user = await db.User.findByPk(req.params.userId); // if (!user) { -// return res -// .status(404) -// .send({ success: false, message: "User not found" }); +// return res.status(404).json({ success: false, message: "User not found" }); // } +// // 2️⃣ Handle password change // if (newPassword) { // const hashedPassword = await bcrypt.hash(newPassword, 10); -// req.body.password = hashedPassword; // Use the hashed password +// req.body.password = hashedPassword; +// delete req.body.newPassword; +// } + +// // 3️⃣ Handle image upload if provided +// let imageUrl = user.image; // Keep old image if none uploaded +// if (req.file) { +// try { +// const result = await cloudinary.uploader.upload(req.file.path, { +// folder: "users", +// public_id: `user_${user.id}_${Date.now()}`, +// transformation: [{ width: 500, height: 500, crop: "fill" }], +// }); +// imageUrl = result.secure_url; +// } catch (uploadErr) { +// console.error("Cloudinary upload failed:", uploadErr); +// return res.status(500).json({ +// success: false, +// message: "Image upload failed", +// }); +// } // } -// // Prepare the update data -// const updateData = { ...req.body }; -// delete updateData.newPassword; // Remove newPassword from the update data +// // 4️⃣ Prepare update data +// const updateData = { ...req.body, image: imageUrl }; -// // Update the user +// // 5️⃣ Update user // const [updatedRows] = await db.User.update(updateData, { // where: { id: req.params.userId }, // }); // if (updatedRows === 0) { // return res -// .status(404) -// .send({ success: false, message: "No updates made" }); +// .status(400) +// .json({ success: false, message: "No updates made" }); // } -// // Fetch the updated user to return +// // 6️⃣ Fetch updated user // const updatedUser = await db.User.findByPk(req.params.userId); -// // console.log(`User with ID ${req.params.userId} updated successfully.`); -// res.status(200).send({ +// res.status(200).json({ // success: true, // message: "User updated successfully", -// user: updatedUser, // Return updated user data +// user: updatedUser, // }); // } catch (error) { -// // console.error("Error updating user:", error); -// res.status(500).send({ success: false, message: "Internal Server Error" }); +// console.error("Error updating user:", error); +// res.status(500).json({ +// success: false, +// message: "Internal Server Error", +// }); // } // }); -router.put("/user/update/:userId", upload.single("image"), async (req, res) => { - try { - const { newPassword } = req.body; - - // 1️⃣ Fetch user - const user = await db.User.findByPk(req.params.userId); - if (!user) { - return res.status(404).json({ success: false, message: "User not found" }); - } - - // 2️⃣ Handle password change - if (newPassword) { - const hashedPassword = await bcrypt.hash(newPassword, 10); - req.body.password = hashedPassword; - delete req.body.newPassword; - } - - // 3️⃣ Handle image upload if provided - let imageUrl = user.image; // Keep old image if none uploaded - if (req.file) { - try { - const result = await cloudinary.uploader.upload(req.file.path, { - folder: "users", - public_id: `user_${user.id}_${Date.now()}`, - transformation: [{ width: 500, height: 500, crop: "fill" }], - }); - imageUrl = result.secure_url; - } catch (uploadErr) { - console.error("Cloudinary upload failed:", uploadErr); - return res.status(500).json({ - success: false, - message: "Image upload failed", - }); - } - } - - // 4️⃣ Prepare update data - const updateData = { ...req.body, image: imageUrl }; - - // 5️⃣ Update user - const [updatedRows] = await db.User.update(updateData, { - where: { id: req.params.userId }, - }); - - if (updatedRows === 0) { - return res - .status(400) - .json({ success: false, message: "No updates made" }); - } - - // 6️⃣ Fetch updated user - const updatedUser = await db.User.findByPk(req.params.userId); - - res.status(200).json({ - success: true, - message: "User updated successfully", - user: updatedUser, - }); - } catch (error) { - console.error("Error updating user:", error); - res.status(500).json({ - success: false, - message: "Internal Server Error", - }); - } -}); - module.exports = router; From bfd913ab711a8acdc4276bd982d1106c40dc2828 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 03:05:04 -0500 Subject: [PATCH 10/16] updates --- controllers/UserAPIRoutes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index cb95002..cdc4ce0 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -66,7 +66,7 @@ router.put("/update/:id", upload.single("profileImage"), async (req, res) => { } // 3️⃣ Handle image upload (optional) - let imageUrl = user.image; // keep previous image by default + let imageUrl = user.profileImage; // keep previous image by default if (req.file) { try { @@ -86,7 +86,7 @@ router.put("/update/:id", upload.single("profileImage"), async (req, res) => { } } - updateData.image = imageUrl; + updateData.profileImage = imageUrl; // 4️⃣ Update the user const [updatedRows] = await db.User.update(updateData, { From ae0cd383075f575f955fb9b0d1300f6c43dc2c69 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 14:17:31 -0500 Subject: [PATCH 11/16] testing --- .../containers/User/Profile/ProfileUpdate.tsx | 575 ++++++++++++++++-- controllers/UserAPIRoutes.js | 132 ++-- 2 files changed, 624 insertions(+), 83 deletions(-) diff --git a/client/src/containers/User/Profile/ProfileUpdate.tsx b/client/src/containers/User/Profile/ProfileUpdate.tsx index d9bfc05..2f2abd7 100644 --- a/client/src/containers/User/Profile/ProfileUpdate.tsx +++ b/client/src/containers/User/Profile/ProfileUpdate.tsx @@ -863,6 +863,473 @@ // export default ProfileUpdate; +// import React, { useEffect, useState } from "react"; +// import axios from "axios"; +// import { +// Button, +// Col, +// Form, +// Row, +// Alert, +// Card, +// Spinner, +// } from "react-bootstrap"; +// import { Link } from "react-router-dom"; + +// type User = { +// id: string; +// email: string; +// password?: string; +// description: string; +// userType: string; +// experienceLevel: string; +// location: string; +// availableFrom: string; +// newPassword?: string; +// profileImage?: string; +// phoneNumber: string; +// driversLicense: string; +// comments: string; +// qrCode?: string; +// }; + +// const ServerPort = +// process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; + +// const ProfileUpdate: React.FC = () => { +// const userId = localStorage.getItem("userId") || ""; + +// const [formData, setFormData] = useState({ +// id: "", +// email: "", +// password: "", +// description: "", +// userType: "", +// experienceLevel: "", +// location: "", +// availableFrom: "", +// newPassword: "", +// profileImage: "", +// phoneNumber: "", +// driversLicense: "", +// comments: "", +// qrCode: "", +// }); + +// const [selectedFile, setSelectedFile] = useState(null); +// const [imagePreview, setImagePreview] = useState(null); +// const [qrCodeImage, setQrCodeImage] = useState(null); +// const [formErrors, setFormErrors] = useState>({}); +// const [loading, setLoading] = useState(false); +// const [isSubmitting, setIsSubmitting] = useState(false); +// const [error, setError] = useState(null); +// const [successMessage, setSuccessMessage] = useState(null); + +// // load user profile +// useEffect(() => { +// const fetchUserData = async () => { +// setLoading(true); +// try { +// const token = localStorage.getItem("token"); +// if (!token) { +// setError("No token found."); +// setLoading(false); +// return; +// } + +// const res = await axios.get(`${ServerPort}/api/user/view/${userId}`, { +// headers: { Authorization: `Bearer ${token}` }, +// }); + +// if (res.status === 200) { +// const user = res.data; +// setFormData({ +// ...user, +// availableFrom: user.availableFrom?.split("T")[0] || "", +// password: "", +// newPassword: "", +// }); +// setImagePreview(user.profileImage || null); +// setQrCodeImage(user.qrPNG || user.qrCode || null); +// } else { +// setError("Failed to fetch user data."); +// } +// } catch (err) { +// handleApiError(err); +// } finally { +// setLoading(false); +// } +// }; + +// if (userId) fetchUserData(); +// // eslint-disable-next-line react-hooks/exhaustive-deps +// }, [userId]); + +// // cleanup object URL when component unmounts / file changes +// useEffect(() => { +// return () => { +// if (imagePreview && imagePreview.startsWith("blob:")) { +// URL.revokeObjectURL(imagePreview); +// } +// }; +// }, [imagePreview]); + +// // handle native file input change (use plain input to avoid bootstrap issues) +// const handleFileChange = (e: React.ChangeEvent) => { +// const file = e.target.files?.[0] ?? null; +// if (file) { +// // revoke previous blob url if any +// if (imagePreview && imagePreview.startsWith("blob:")) { +// URL.revokeObjectURL(imagePreview); +// } +// setSelectedFile(file); +// setImagePreview(URL.createObjectURL(file)); +// // don't mutate formData.profileImage here — backend will return permanent URL +// } else { +// setSelectedFile(null); +// } +// }; + +// const validateForm = () => { +// const errors: Record = {}; +// const required = [ +// "email", +// "description", +// "userType", +// "experienceLevel", +// "location", +// "availableFrom", +// "phoneNumber", +// "driversLicense", +// "comments", +// ] as const; + +// for (const field of required) { +// // @ts-ignore +// if (!formData[field]) errors[field] = "This field is required"; +// } + +// if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) { +// errors.email = "Invalid email"; +// } + +// setFormErrors(errors); +// return Object.keys(errors).length === 0; +// }; + +// // Submit: if selectedFile exists -> send multipart/form-data to backend, +// // otherwise send JSON. Backend should accept multipart and handle saving to Cloudinary. +// const handleUserUpdate = async (e: React.FormEvent) => { +// e.preventDefault(); +// setError(null); +// setSuccessMessage(null); + +// if (!validateForm()) return; + +// setLoading(true); +// setIsSubmitting(true); + +// try { +// const token = localStorage.getItem("token"); +// if (!token) throw new Error("No token - please sign in."); + +// if (selectedFile) { +// // send multipart to backend; backend must use multer to accept `profileImage` +// const form = new FormData(); + +// // append scalar fields +// Object.entries(formData).forEach(([k, v]) => { +// if (v !== undefined && v !== null) form.append(k, String(v)); +// }); + +// form.append("profileImage", selectedFile); + +// const res = await axios.put( +// `${ServerPort}/api/user/update/${userId}`, +// form, +// { +// headers: { +// Authorization: `Bearer ${token}`, +// "Content-Type": "multipart/form-data", +// }, +// } +// ); + +// if (res.status === 200) { +// const updatedUser = res.data.user ?? res.data; +// setFormData((prev) => ({ +// ...prev, +// ...updatedUser, +// newPassword: "", +// })); +// // server should return permanent image URL (profileImage) +// setImagePreview(updatedUser.profileImage || imagePreview); +// setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); +// setSuccessMessage("Profile updated and image uploaded."); +// setSelectedFile(null); +// } else { +// setError("Unexpected server response when uploading image."); +// } +// } else { +// // no file selected — send JSON update +// const payload: Partial = { ...formData }; +// if (!formData.newPassword) delete payload.password; + +// const res = await axios.put( +// `${ServerPort}/api/user/update/${userId}`, +// payload, +// { +// headers: { +// Authorization: `Bearer ${token}`, +// "Content-Type": "application/json", +// }, +// } +// ); + +// if (res.status === 200) { +// const updatedUser = res.data.user ?? res.data; +// setFormData((prev) => ({ +// ...prev, +// ...updatedUser, +// newPassword: "", +// })); +// setImagePreview(updatedUser.profileImage || imagePreview); +// setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); +// setSuccessMessage("Profile updated."); +// } else { +// setError("Unexpected server response when updating profile."); +// } +// } +// } catch (err) { +// handleApiError(err); +// } finally { +// setIsSubmitting(false); +// setLoading(false); +// } +// }; + +// const handleApiError = (err: any) => { +// if (axios.isAxiosError(err)) { +// const msg = +// (err.response && (err.response.data?.message || err.response.data)) || +// err.message || +// "Server error"; +// setError(String(msg)); +// } else { +// setError(String(err || "Unknown error")); +// } +// }; + +// return ( +// +//

Update Profile

+ +// {loading && ( +// +// )} +// {error && {error}} +// {successMessage && {successMessage}} + +//
+// {/* IMAGE PREVIEW + NATIVE FILE INPUT (avoid Form.Control for file) */} +//
+// Profile +//
+// +// {selectedFile && ( +//
+// Selected: {selectedFile.name} +//
+// )} +//
+//
+ +// {/* QR CODE */} +// {qrCodeImage && ( +//
+//
Your QR Code
+// QR Code +//
+// )} + +// {/* FORM FIELDS */} +// +// +// Email +// setFormData({ ...formData, email: e.target.value })} +// /> +// +// {formErrors.email} +// +// + +// +// Description +// setFormData({ ...formData, description: e.target.value })} +// /> +// +// {formErrors.description} +// +// +// + +// +// +// User Type +// setFormData({ ...formData, userType: e.target.value })} +// /> +// +// {formErrors.userType} +// +// + +// +// Experience Level +// +// setFormData({ ...formData, experienceLevel: e.target.value }) +// } +// /> +// +// {formErrors.experienceLevel} +// +// +// + +// +// +// Location +// setFormData({ ...formData, location: e.target.value })} +// /> +// +// {formErrors.location} +// +// + +// +// Available From +// setFormData({ ...formData, availableFrom: e.target.value })} +// /> +// +// {formErrors.availableFrom} +// +// +// + +// +// +// Phone Number +// setFormData({ ...formData, phoneNumber: e.target.value })} +// /> +// +// {formErrors.phoneNumber} +// +// + +// +// Driver’s License +// setFormData({ ...formData, driversLicense: e.target.value })} +// /> +// +// {formErrors.driversLicense} +// +// +// + +// +// Comments +// setFormData({ ...formData, comments: e.target.value })} +// /> +// +// {formErrors.comments} +// +// + +// +// New Password +// setFormData({ ...formData, newPassword: e.target.value })} +// /> +// + +//
+// +// +// +// +//
+//
+//
+// ); +// }; + +// export default ProfileUpdate; + import React, { useEffect, useState } from "react"; import axios from "axios"; import { @@ -925,7 +1392,6 @@ const ProfileUpdate: React.FC = () => { const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); - // load user profile useEffect(() => { const fetchUserData = async () => { setLoading(true); @@ -962,10 +1428,8 @@ const ProfileUpdate: React.FC = () => { }; if (userId) fetchUserData(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [userId]); - // cleanup object URL when component unmounts / file changes useEffect(() => { return () => { if (imagePreview && imagePreview.startsWith("blob:")) { @@ -974,17 +1438,14 @@ const ProfileUpdate: React.FC = () => { }; }, [imagePreview]); - // handle native file input change (use plain input to avoid bootstrap issues) const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] ?? null; if (file) { - // revoke previous blob url if any if (imagePreview && imagePreview.startsWith("blob:")) { URL.revokeObjectURL(imagePreview); } setSelectedFile(file); setImagePreview(URL.createObjectURL(file)); - // don't mutate formData.profileImage here — backend will return permanent URL } else { setSelectedFile(null); } @@ -1017,8 +1478,6 @@ const ProfileUpdate: React.FC = () => { return Object.keys(errors).length === 0; }; - // Submit: if selectedFile exists -> send multipart/form-data to backend, - // otherwise send JSON. Backend should accept multipart and handle saving to Cloudinary. const handleUserUpdate = async (e: React.FormEvent) => { e.preventDefault(); setError(null); @@ -1034,14 +1493,10 @@ const ProfileUpdate: React.FC = () => { if (!token) throw new Error("No token - please sign in."); if (selectedFile) { - // send multipart to backend; backend must use multer to accept `profileImage` const form = new FormData(); - - // append scalar fields Object.entries(formData).forEach(([k, v]) => { if (v !== undefined && v !== null) form.append(k, String(v)); }); - form.append("profileImage", selectedFile); const res = await axios.put( @@ -1062,7 +1517,6 @@ const ProfileUpdate: React.FC = () => { ...updatedUser, newPassword: "", })); - // server should return permanent image URL (profileImage) setImagePreview(updatedUser.profileImage || imagePreview); setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); setSuccessMessage("Profile updated and image uploaded."); @@ -1071,7 +1525,6 @@ const ProfileUpdate: React.FC = () => { setError("Unexpected server response when uploading image."); } } else { - // no file selected — send JSON update const payload: Partial = { ...formData }; if (!formData.newPassword) delete payload.password; @@ -1131,7 +1584,6 @@ const ProfileUpdate: React.FC = () => { {successMessage && {successMessage}}
- {/* IMAGE PREVIEW + NATIVE FILE INPUT (avoid Form.Control for file) */}
{
- {/* QR CODE */} {qrCodeImage && (
Your QR Code
@@ -1177,7 +1628,6 @@ const ProfileUpdate: React.FC = () => {
)} - {/* FORM FIELDS */} Email @@ -1250,13 +1700,16 @@ const ProfileUpdate: React.FC = () => { + {/* COMPLETED FROM YOUR TRUNCATION POINT */} Available From setFormData({ ...formData, availableFrom: e.target.value })} + onChange={(e) => + setFormData({ ...formData, availableFrom: e.target.value }) + } /> {formErrors.availableFrom} @@ -1264,6 +1717,7 @@ const ProfileUpdate: React.FC = () => { + {/* PHONE & DRIVERS LICENSE */} Phone Number @@ -1279,12 +1733,14 @@ const ProfileUpdate: React.FC = () => { - Driver’s License + Driver's License setFormData({ ...formData, driversLicense: e.target.value })} + onChange={(e) => + setFormData({ ...formData, driversLicense: e.target.value }) + } /> {formErrors.driversLicense} @@ -1292,36 +1748,61 @@ const ProfileUpdate: React.FC = () => { - - Comments - setFormData({ ...formData, comments: e.target.value })} - /> - - {formErrors.comments} - - - - - New Password - setFormData({ ...formData, newPassword: e.target.value })} - /> - + {/* COMMENTS */} + + + Additional Comments + setFormData({ ...formData, comments: e.target.value })} + /> + + {formErrors.comments} + + + + + {/* PASSWORD UPDATE */} + + + Current Password + setFormData({ ...formData, password: e.target.value })} + /> + -
- - - - +
+ + {/* BACK BUTTON */} +
+ Back to Profile
diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index cdc4ce0..f10765a 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -48,67 +48,127 @@ router.get("/view/:id", (req, res) => { }); }); +// router.put("/update/:id", upload.single("profileImage"), async (req, res) => { +// try { +// // 1️⃣ Fetch the user +// const user = await db.User.findByPk(req.params.id); +// if (!user) { +// return res.status(404).send({ success: false, message: "User not found" }); +// } + +// // 2️⃣ Handle password update +// const updateData = { ...req.body }; + +// if (updateData.newPassword) { +// const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); +// updateData.password = hashedPassword; +// delete updateData.newPassword; +// } + +// // 3️⃣ Handle image upload (optional) +// let imageUrl = user.profileImage; // keep previous image by default + +// if (req.file) { +// try { +// const result = await cloudinary.uploader.upload(req.file.path, { +// folder: "users", +// public_id: `user_${user.id}_${Date.now()}`, +// transformation: [{ width: 500, height: 500, crop: "fill" }], +// }); + +// imageUrl = result.secure_url; +// } catch (err) { +// console.error("Cloudinary Upload Error:", err); +// return res.status(500).json({ +// success: false, +// message: "Image upload failed", +// }); +// } +// } + +// updateData.profileImage = imageUrl; + +// // 4️⃣ Update the user +// const [updatedRows] = await db.User.update(updateData, { +// where: { id: req.params.id }, +// }); + +// if (updatedRows === 0) { +// return res.status(404).send({ success: false, message: "No updates made" }); +// } + +// // 5️⃣ Fetch updated user +// const updatedUser = await db.User.findByPk(req.params.id); + +// res.status(200).send({ +// success: true, +// message: "User updated successfully", +// user: updatedUser, +// }); + +// } catch (error) { +// console.error("Error updating user:", error); +// res.status(500).send({ success: false, message: "Internal Server Error" }); +// } +// }); router.put("/update/:id", upload.single("profileImage"), async (req, res) => { try { - // 1️⃣ Fetch the user const user = await db.User.findByPk(req.params.id); if (!user) { - return res.status(404).send({ success: false, message: "User not found" }); - } - - // 2️⃣ Handle password update - const updateData = { ...req.body }; - - if (updateData.newPassword) { - const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); - updateData.password = hashedPassword; - delete updateData.newPassword; + return res.status(404).json({ success: false, message: "User not found" }); } - // 3️⃣ Handle image upload (optional) - let imageUrl = user.profileImage; // keep previous image by default + let cloudinaryImageUrl = user.profileImage; // keep old image if none uploaded + // 1️⃣ If frontend uploaded an image file, upload it to Cloudinary FIRST if (req.file) { try { - const result = await cloudinary.uploader.upload(req.file.path, { - folder: "users", - public_id: `user_${user.id}_${Date.now()}`, - transformation: [{ width: 500, height: 500, crop: "fill" }], - }); - - imageUrl = result.secure_url; + const uploadResult = await cloudinary.uploader.upload(req.file.path); + cloudinaryImageUrl = uploadResult.secure_url; } catch (err) { console.error("Cloudinary Upload Error:", err); return res.status(500).json({ success: false, message: "Image upload failed", + error: err, }); } } - updateData.profileImage = imageUrl; - - // 4️⃣ Update the user - const [updatedRows] = await db.User.update(updateData, { - where: { id: req.params.id }, - }); + // 2️⃣ Build update object (don’t overwrite fields that weren’t sent) + const updateData = { + email: req.body.email ?? user.email, + description: req.body.description ?? user.description, + userType: req.body.userType ?? user.userType, + experienceLevel: req.body.experienceLevel ?? user.experienceLevel, + location: req.body.location ?? user.location, + availableFrom: req.body.availableFrom ?? user.availableFrom, + phoneNumber: req.body.phoneNumber ?? user.phoneNumber, + driversLicense: req.body.driversLicense ?? user.driversLicense, + comments: req.body.comments ?? user.comments, + profileImage: cloudinaryImageUrl, // ⬅️ ALWAYS FINAL IMAGE HERE + }; - if (updatedRows === 0) { - return res.status(404).send({ success: false, message: "No updates made" }); + // 3️⃣ Handle password update + if (req.body.newPassword) { + const bcrypt = require("bcryptjs"); + const hashed = await bcrypt.hash(req.body.newPassword, 10); + updateData.password = hashed; } - // 5️⃣ Fetch updated user - const updatedUser = await db.User.findByPk(req.params.id); + // 4️⃣ Update DB + await user.update(updateData); - res.status(200).send({ + return res.status(200).json({ success: true, - message: "User updated successfully", - user: updatedUser, + message: "Profile updated successfully", + user, + image: cloudinaryImageUrl, }); - } catch (error) { - console.error("Error updating user:", error); - res.status(500).send({ success: false, message: "Internal Server Error" }); + } catch (err) { + console.error("Profile Update Error:", err); + return res.status(500).json({ success: false, error: err }); } }); From 7eb4eabffec080bf94153971c902b48bdd059092 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 15:09:09 -0500 Subject: [PATCH 12/16] updates --- .../containers/User/Profile/ProfileUpdate.tsx | 1272 +++-------------- 1 file changed, 162 insertions(+), 1110 deletions(-) diff --git a/client/src/containers/User/Profile/ProfileUpdate.tsx b/client/src/containers/User/Profile/ProfileUpdate.tsx index 2f2abd7..bda31f6 100644 --- a/client/src/containers/User/Profile/ProfileUpdate.tsx +++ b/client/src/containers/User/Profile/ProfileUpdate.tsx @@ -428,909 +428,7 @@ // export default ProfileUpdate; -// import React, { useState, useEffect } from "react"; -// import axios from "axios"; -// import { -// Button, -// Col, -// Form, -// Row, -// Alert, -// Card, -// Spinner, -// } from "react-bootstrap"; - -// type User = { -// id: string; -// email: string; -// password?: string; -// description: string; -// userType: string; -// experienceLevel: string; -// location: string; -// availableFrom: string; -// newPassword?: string; -// profileImage?: string; -// phoneNumber: string; -// driversLicense: string; -// comments: string; -// qrCode?: string; -// }; - -// const ServerPort = -// process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; - -// const CLOUDINARY_UPLOAD_URL = `https://api.cloudinary.com/v1_1/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload`; -// const CLOUDINARY_UPLOAD_PRESET = process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET; - -// const ProfileUpdate: React.FC = () => { -// const userId = localStorage.getItem("userId"); - -// const [formData, setFormData] = useState({ -// id: "", -// email: "", -// password: "", -// description: "", -// userType: "", -// experienceLevel: "", -// location: "", -// availableFrom: "", -// newPassword: "", -// profileImage: "", -// phoneNumber: "", -// driversLicense: "", -// comments: "", -// qrCode: "", -// }); - -// const [imagePreview, setImagePreview] = useState(null); -// const [qrCodeImage, setQrCodeImage] = useState(null); -// const [formErrors, setFormErrors] = useState>({}); -// const [loading, setLoading] = useState(false); -// const [isSubmitting, setIsSubmitting] = useState(false); -// const [error, setError] = useState(null); -// const [successMessage, setSuccessMessage] = useState(null); - -// // ------------------------- -// // LOAD USER PROFILE -// // ------------------------- -// useEffect(() => { -// const fetchUserData = async () => { -// setLoading(true); - -// try { -// const token = localStorage.getItem("token"); -// if (!token) return setError("No token found."); - -// const response = await axios.get( -// `${ServerPort}/api/user/view/${userId}`, -// { headers: { Authorization: `Bearer ${token}` } } -// ); - -// const user = response.data; - -// setFormData({ -// ...user, -// availableFrom: user.availableFrom?.split("T")[0] || "", -// password: "", -// newPassword: "", -// }); - -// setImagePreview(user.profileImage || null); -// setQrCodeImage(user.qrCode || null); -// } catch (err) { -// handleApiError(err); -// } - -// setLoading(false); -// }; - -// if (userId) fetchUserData(); -// }, [userId]); - -// // ------------------------- -// // IMAGE UPLOAD TO CLOUDINARY -// // ------------------------- -// const handleImageUpload = async (file: File) => { -// setLoading(true); - -// try { -// const data = new FormData(); -// data.append("file", file); -// data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!); - -// const uploadRes = await axios.post(CLOUDINARY_UPLOAD_URL, data); -// const secureUrl = uploadRes.data.secure_url; - -// setFormData((prev) => ({ ...prev, profileImage: secureUrl })); -// setImagePreview(secureUrl); -// } catch (err) { -// setError("Image upload failed. Try again."); -// } - -// setLoading(false); -// }; - -// // ------------------------- -// // FORM VALIDATION -// // ------------------------- -// const validateForm = () => { -// const errors: Record = {}; -// const required = [ -// "email", -// "description", -// "userType", -// "experienceLevel", -// "location", -// "availableFrom", -// "phoneNumber", -// "driversLicense", -// "comments", -// ]; - -// required.forEach((field) => { -// // @ts-ignore -// if (!formData[field]) errors[field] = "This field is required"; -// }); - -// if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) -// errors.email = "Invalid email"; - -// setFormErrors(errors); - -// return Object.keys(errors).length === 0; -// }; - -// // ------------------------- -// // UPDATE USER -// // ------------------------- -// const handleUserUpdate = async (e: React.FormEvent) => { -// e.preventDefault(); -// if (!validateForm()) return; - -// setLoading(true); -// setIsSubmitting(true); -// setError(null); -// setSuccessMessage(null); - -// try { -// const token = localStorage.getItem("token"); - -// const payload = { ...formData }; -// if (!formData.newPassword) delete payload.password; - -// const res = await axios.put( -// `${ServerPort}/api/user/update/${userId}`, -// payload, -// { -// headers: { -// Authorization: `Bearer ${token}`, -// "Content-Type": "application/json", -// }, -// } -// ); - -// if (res.status === 200) { -// setSuccessMessage("Profile successfully updated!"); -// setQrCodeImage(res.data.qrCode || qrCodeImage); -// setFormData((prev) => ({ ...prev, newPassword: "" })); -// } -// } catch (err) { -// handleApiError(err); -// } - -// setIsSubmitting(false); -// setLoading(false); -// }; - -// // ------------------------- -// // ERROR HANDLER -// // ------------------------- -// const handleApiError = (error: any) => { -// if (axios.isAxiosError(error)) { -// setError(error.response?.data?.message || "Something went wrong."); -// } else { -// setError("An unexpected error occurred."); -// } -// }; - -// // ------------------------- -// // UI RENDER -// // ------------------------- -// return ( -// -//

Update Profile

- -// {loading && ( -// -// )} -// {error && {error}} -// {successMessage && {successMessage}} - -//
-// {/* PROFILE IMAGE UPLOAD */} -//
-// Profile - -// ) => { -// const file = e.target.files?.[0]; -// if (file) handleImageUpload(file); -// }} -// /> -//
- -// {/* QR CODE */} -// {qrCodeImage && ( -//
-//
Your QR Code
-// QR Code -//
-// )} - -// {/* FORM ROWS */} -// -// -// Email -// -// setFormData({ ...formData, email: e.target.value }) -// } -// /> -// -// {formErrors.email} -// -// - -// -// Description -// -// setFormData({ ...formData, description: e.target.value }) -// } -// /> -// -// {formErrors.description} -// -// -// - -// -// -// User Type -// -// setFormData({ ...formData, userType: e.target.value }) -// } -// /> -// -// {formErrors.userType} -// -// - -// -// Experience Level -// -// setFormData({ -// ...formData, -// experienceLevel: e.target.value, -// }) -// } -// /> -// -// {formErrors.experienceLevel} -// -// -// - -// -// -// Location -// -// setFormData({ ...formData, location: e.target.value }) -// } -// /> -// -// {formErrors.location} -// -// - -// -// Available From -// -// setFormData({ ...formData, availableFrom: e.target.value }) -// } -// /> -// -// {formErrors.availableFrom} -// -// -// - -// -// -// Phone Number -// -// setFormData({ ...formData, phoneNumber: e.target.value }) -// } -// /> -// -// {formErrors.phoneNumber} -// -// - -// -// Driver’s License -// -// setFormData({ -// ...formData, -// driversLicense: e.target.value, -// }) -// } -// /> -// -// {formErrors.driversLicense} -// -// -// - -// -// Comments -// -// setFormData({ ...formData, comments: e.target.value }) -// } -// /> -// -// {formErrors.comments} -// -// - -// -// New Password -// -// setFormData({ ...formData, newPassword: e.target.value }) -// } -// /> -// - -//
-// -//
-//
-//
-// ); -// }; - -// export default ProfileUpdate; - -// import React, { useEffect, useState } from "react"; -// import axios from "axios"; -// import { -// Button, -// Col, -// Form, -// Row, -// Alert, -// Card, -// Spinner, -// } from "react-bootstrap"; -// import { Link } from "react-router-dom"; - -// type User = { -// id: string; -// email: string; -// password?: string; -// description: string; -// userType: string; -// experienceLevel: string; -// location: string; -// availableFrom: string; -// newPassword?: string; -// profileImage?: string; -// phoneNumber: string; -// driversLicense: string; -// comments: string; -// qrCode?: string; -// }; - -// const ServerPort = -// process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; - -// const ProfileUpdate: React.FC = () => { -// const userId = localStorage.getItem("userId") || ""; - -// const [formData, setFormData] = useState({ -// id: "", -// email: "", -// password: "", -// description: "", -// userType: "", -// experienceLevel: "", -// location: "", -// availableFrom: "", -// newPassword: "", -// profileImage: "", -// phoneNumber: "", -// driversLicense: "", -// comments: "", -// qrCode: "", -// }); - -// const [selectedFile, setSelectedFile] = useState(null); -// const [imagePreview, setImagePreview] = useState(null); -// const [qrCodeImage, setQrCodeImage] = useState(null); -// const [formErrors, setFormErrors] = useState>({}); -// const [loading, setLoading] = useState(false); -// const [isSubmitting, setIsSubmitting] = useState(false); -// const [error, setError] = useState(null); -// const [successMessage, setSuccessMessage] = useState(null); - -// // load user profile -// useEffect(() => { -// const fetchUserData = async () => { -// setLoading(true); -// try { -// const token = localStorage.getItem("token"); -// if (!token) { -// setError("No token found."); -// setLoading(false); -// return; -// } - -// const res = await axios.get(`${ServerPort}/api/user/view/${userId}`, { -// headers: { Authorization: `Bearer ${token}` }, -// }); - -// if (res.status === 200) { -// const user = res.data; -// setFormData({ -// ...user, -// availableFrom: user.availableFrom?.split("T")[0] || "", -// password: "", -// newPassword: "", -// }); -// setImagePreview(user.profileImage || null); -// setQrCodeImage(user.qrPNG || user.qrCode || null); -// } else { -// setError("Failed to fetch user data."); -// } -// } catch (err) { -// handleApiError(err); -// } finally { -// setLoading(false); -// } -// }; - -// if (userId) fetchUserData(); -// // eslint-disable-next-line react-hooks/exhaustive-deps -// }, [userId]); - -// // cleanup object URL when component unmounts / file changes -// useEffect(() => { -// return () => { -// if (imagePreview && imagePreview.startsWith("blob:")) { -// URL.revokeObjectURL(imagePreview); -// } -// }; -// }, [imagePreview]); - -// // handle native file input change (use plain input to avoid bootstrap issues) -// const handleFileChange = (e: React.ChangeEvent) => { -// const file = e.target.files?.[0] ?? null; -// if (file) { -// // revoke previous blob url if any -// if (imagePreview && imagePreview.startsWith("blob:")) { -// URL.revokeObjectURL(imagePreview); -// } -// setSelectedFile(file); -// setImagePreview(URL.createObjectURL(file)); -// // don't mutate formData.profileImage here — backend will return permanent URL -// } else { -// setSelectedFile(null); -// } -// }; - -// const validateForm = () => { -// const errors: Record = {}; -// const required = [ -// "email", -// "description", -// "userType", -// "experienceLevel", -// "location", -// "availableFrom", -// "phoneNumber", -// "driversLicense", -// "comments", -// ] as const; - -// for (const field of required) { -// // @ts-ignore -// if (!formData[field]) errors[field] = "This field is required"; -// } - -// if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) { -// errors.email = "Invalid email"; -// } - -// setFormErrors(errors); -// return Object.keys(errors).length === 0; -// }; - -// // Submit: if selectedFile exists -> send multipart/form-data to backend, -// // otherwise send JSON. Backend should accept multipart and handle saving to Cloudinary. -// const handleUserUpdate = async (e: React.FormEvent) => { -// e.preventDefault(); -// setError(null); -// setSuccessMessage(null); - -// if (!validateForm()) return; - -// setLoading(true); -// setIsSubmitting(true); - -// try { -// const token = localStorage.getItem("token"); -// if (!token) throw new Error("No token - please sign in."); - -// if (selectedFile) { -// // send multipart to backend; backend must use multer to accept `profileImage` -// const form = new FormData(); - -// // append scalar fields -// Object.entries(formData).forEach(([k, v]) => { -// if (v !== undefined && v !== null) form.append(k, String(v)); -// }); - -// form.append("profileImage", selectedFile); - -// const res = await axios.put( -// `${ServerPort}/api/user/update/${userId}`, -// form, -// { -// headers: { -// Authorization: `Bearer ${token}`, -// "Content-Type": "multipart/form-data", -// }, -// } -// ); - -// if (res.status === 200) { -// const updatedUser = res.data.user ?? res.data; -// setFormData((prev) => ({ -// ...prev, -// ...updatedUser, -// newPassword: "", -// })); -// // server should return permanent image URL (profileImage) -// setImagePreview(updatedUser.profileImage || imagePreview); -// setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); -// setSuccessMessage("Profile updated and image uploaded."); -// setSelectedFile(null); -// } else { -// setError("Unexpected server response when uploading image."); -// } -// } else { -// // no file selected — send JSON update -// const payload: Partial = { ...formData }; -// if (!formData.newPassword) delete payload.password; - -// const res = await axios.put( -// `${ServerPort}/api/user/update/${userId}`, -// payload, -// { -// headers: { -// Authorization: `Bearer ${token}`, -// "Content-Type": "application/json", -// }, -// } -// ); - -// if (res.status === 200) { -// const updatedUser = res.data.user ?? res.data; -// setFormData((prev) => ({ -// ...prev, -// ...updatedUser, -// newPassword: "", -// })); -// setImagePreview(updatedUser.profileImage || imagePreview); -// setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); -// setSuccessMessage("Profile updated."); -// } else { -// setError("Unexpected server response when updating profile."); -// } -// } -// } catch (err) { -// handleApiError(err); -// } finally { -// setIsSubmitting(false); -// setLoading(false); -// } -// }; - -// const handleApiError = (err: any) => { -// if (axios.isAxiosError(err)) { -// const msg = -// (err.response && (err.response.data?.message || err.response.data)) || -// err.message || -// "Server error"; -// setError(String(msg)); -// } else { -// setError(String(err || "Unknown error")); -// } -// }; - -// return ( -// -//

Update Profile

- -// {loading && ( -// -// )} -// {error && {error}} -// {successMessage && {successMessage}} - -//
-// {/* IMAGE PREVIEW + NATIVE FILE INPUT (avoid Form.Control for file) */} -//
-// Profile -//
-// -// {selectedFile && ( -//
-// Selected: {selectedFile.name} -//
-// )} -//
-//
- -// {/* QR CODE */} -// {qrCodeImage && ( -//
-//
Your QR Code
-// QR Code -//
-// )} - -// {/* FORM FIELDS */} -// -// -// Email -// setFormData({ ...formData, email: e.target.value })} -// /> -// -// {formErrors.email} -// -// - -// -// Description -// setFormData({ ...formData, description: e.target.value })} -// /> -// -// {formErrors.description} -// -// -// - -// -// -// User Type -// setFormData({ ...formData, userType: e.target.value })} -// /> -// -// {formErrors.userType} -// -// - -// -// Experience Level -// -// setFormData({ ...formData, experienceLevel: e.target.value }) -// } -// /> -// -// {formErrors.experienceLevel} -// -// -// - -// -// -// Location -// setFormData({ ...formData, location: e.target.value })} -// /> -// -// {formErrors.location} -// -// - -// -// Available From -// setFormData({ ...formData, availableFrom: e.target.value })} -// /> -// -// {formErrors.availableFrom} -// -// -// - -// -// -// Phone Number -// setFormData({ ...formData, phoneNumber: e.target.value })} -// /> -// -// {formErrors.phoneNumber} -// -// - -// -// Driver’s License -// setFormData({ ...formData, driversLicense: e.target.value })} -// /> -// -// {formErrors.driversLicense} -// -// -// - -// -// Comments -// setFormData({ ...formData, comments: e.target.value })} -// /> -// -// {formErrors.comments} -// -// - -// -// New Password -// setFormData({ ...formData, newPassword: e.target.value })} -// /> -// - -//
-// -// -// -// -//
-//
-//
-// ); -// }; - -// export default ProfileUpdate; - -import React, { useEffect, useState } from "react"; +import React, { useState, useEffect } from "react"; import axios from "axios"; import { Button, @@ -1341,7 +439,6 @@ import { Card, Spinner, } from "react-bootstrap"; -import { Link } from "react-router-dom"; type User = { id: string; @@ -1363,8 +460,11 @@ type User = { const ServerPort = process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; +const CLOUDINARY_UPLOAD_URL = `https://api.cloudinary.com/v1_1/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload`; +const CLOUDINARY_UPLOAD_PRESET = process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET; + const ProfileUpdate: React.FC = () => { - const userId = localStorage.getItem("userId") || ""; + const userId = localStorage.getItem("userId"); const [formData, setFormData] = useState({ id: "", @@ -1383,7 +483,6 @@ const ProfileUpdate: React.FC = () => { qrCode: "", }); - const [selectedFile, setSelectedFile] = useState(null); const [imagePreview, setImagePreview] = useState(null); const [qrCodeImage, setQrCodeImage] = useState(null); const [formErrors, setFormErrors] = useState>({}); @@ -1392,65 +491,69 @@ const ProfileUpdate: React.FC = () => { const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); + // ------------------------- + // LOAD USER PROFILE + // ------------------------- useEffect(() => { const fetchUserData = async () => { setLoading(true); + try { const token = localStorage.getItem("token"); - if (!token) { - setError("No token found."); - setLoading(false); - return; - } + if (!token) return setError("No token found."); + + const response = await axios.get( + `${ServerPort}/api/user/view/${userId}`, + { headers: { Authorization: `Bearer ${token}` } } + ); + + const user = response.data; - const res = await axios.get(`${ServerPort}/api/user/view/${userId}`, { - headers: { Authorization: `Bearer ${token}` }, + setFormData({ + ...user, + availableFrom: user.availableFrom?.split("T")[0] || "", + password: "", + newPassword: "", }); - if (res.status === 200) { - const user = res.data; - setFormData({ - ...user, - availableFrom: user.availableFrom?.split("T")[0] || "", - password: "", - newPassword: "", - }); - setImagePreview(user.profileImage || null); - setQrCodeImage(user.qrPNG || user.qrCode || null); - } else { - setError("Failed to fetch user data."); - } + setImagePreview(user.profileImage || null); + setQrCodeImage(user.qrCode || null); } catch (err) { handleApiError(err); - } finally { - setLoading(false); } + + setLoading(false); }; if (userId) fetchUserData(); }, [userId]); - useEffect(() => { - return () => { - if (imagePreview && imagePreview.startsWith("blob:")) { - URL.revokeObjectURL(imagePreview); - } - }; - }, [imagePreview]); + // ------------------------- + // IMAGE UPLOAD TO CLOUDINARY + // ------------------------- + const handleImageUpload = async (file: File) => { + setLoading(true); - const handleFileChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0] ?? null; - if (file) { - if (imagePreview && imagePreview.startsWith("blob:")) { - URL.revokeObjectURL(imagePreview); - } - setSelectedFile(file); - setImagePreview(URL.createObjectURL(file)); - } else { - setSelectedFile(null); + try { + const data = new FormData(); + data.append("file", file); + data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!); + + const uploadRes = await axios.post(CLOUDINARY_UPLOAD_URL, data); + const secureUrl = uploadRes.data.secure_url; + + setFormData((prev) => ({ ...prev, profileImage: secureUrl })); + setImagePreview(secureUrl); + } catch (err) { + setError("Image upload failed. Try again."); } + + setLoading(false); }; + // ------------------------- + // FORM VALIDATION + // ------------------------- const validateForm = () => { const errors: Record = {}; const required = [ @@ -1463,116 +566,77 @@ const ProfileUpdate: React.FC = () => { "phoneNumber", "driversLicense", "comments", - ] as const; + ]; - for (const field of required) { + required.forEach((field) => { // @ts-ignore if (!formData[field]) errors[field] = "This field is required"; - } + }); - if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) { + if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) errors.email = "Invalid email"; - } setFormErrors(errors); + return Object.keys(errors).length === 0; }; + // ------------------------- + // UPDATE USER + // ------------------------- const handleUserUpdate = async (e: React.FormEvent) => { e.preventDefault(); - setError(null); - setSuccessMessage(null); - if (!validateForm()) return; setLoading(true); setIsSubmitting(true); + setError(null); + setSuccessMessage(null); try { const token = localStorage.getItem("token"); - if (!token) throw new Error("No token - please sign in."); - - if (selectedFile) { - const form = new FormData(); - Object.entries(formData).forEach(([k, v]) => { - if (v !== undefined && v !== null) form.append(k, String(v)); - }); - form.append("profileImage", selectedFile); - - const res = await axios.put( - `${ServerPort}/api/user/update/${userId}`, - form, - { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "multipart/form-data", - }, - } - ); - if (res.status === 200) { - const updatedUser = res.data.user ?? res.data; - setFormData((prev) => ({ - ...prev, - ...updatedUser, - newPassword: "", - })); - setImagePreview(updatedUser.profileImage || imagePreview); - setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); - setSuccessMessage("Profile updated and image uploaded."); - setSelectedFile(null); - } else { - setError("Unexpected server response when uploading image."); + const payload = { ...formData }; + if (!formData.newPassword) delete payload.password; + + const res = await axios.put( + `${ServerPort}/api/user/update/${userId}`, + payload, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, } - } else { - const payload: Partial = { ...formData }; - if (!formData.newPassword) delete payload.password; - - const res = await axios.put( - `${ServerPort}/api/user/update/${userId}`, - payload, - { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - } - ); + ); - if (res.status === 200) { - const updatedUser = res.data.user ?? res.data; - setFormData((prev) => ({ - ...prev, - ...updatedUser, - newPassword: "", - })); - setImagePreview(updatedUser.profileImage || imagePreview); - setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); - setSuccessMessage("Profile updated."); - } else { - setError("Unexpected server response when updating profile."); - } + if (res.status === 200) { + setSuccessMessage("Profile successfully updated!"); + setQrCodeImage(res.data.qrCode || qrCodeImage); + setFormData((prev) => ({ ...prev, newPassword: "" })); } } catch (err) { handleApiError(err); - } finally { - setIsSubmitting(false); - setLoading(false); } + + setIsSubmitting(false); + setLoading(false); }; - const handleApiError = (err: any) => { - if (axios.isAxiosError(err)) { - const msg = - (err.response && (err.response.data?.message || err.response.data)) || - err.message || - "Server error"; - setError(String(msg)); + // ------------------------- + // ERROR HANDLER + // ------------------------- + const handleApiError = (error: any) => { + if (axios.isAxiosError(error)) { + setError(error.response?.data?.message || "Something went wrong."); } else { - setError(String(err || "Unknown error")); + setError("An unexpected error occurred."); } }; + // ------------------------- + // UI RENDER + // ------------------------- return (

Update Profile

@@ -1583,12 +647,12 @@ const ProfileUpdate: React.FC = () => { {error && {error}} {successMessage && {successMessage}} -
+ + {/* PROFILE IMAGE UPLOAD */}
Profile { border: "3px solid #ddd", }} /> -
- - {selectedFile && ( -
- Selected: {selectedFile.name} -
- )} -
+ + ) => { + const file = e.target.files?.[0]; + if (file) handleImageUpload(file); + }} + />
+ {/* QR CODE */} {qrCodeImage && (
Your QR Code
- QR Code + QR Code
)} + {/* FORM ROWS */} - + Email setFormData({ ...formData, email: e.target.value })} + onChange={(e) => + setFormData({ ...formData, email: e.target.value }) + } /> {formErrors.email} - + Description setFormData({ ...formData, description: e.target.value })} + onChange={(e) => + setFormData({ ...formData, description: e.target.value }) + } /> {formErrors.description} @@ -1657,27 +718,32 @@ const ProfileUpdate: React.FC = () => { - + User Type setFormData({ ...formData, userType: e.target.value })} + onChange={(e) => + setFormData({ ...formData, userType: e.target.value }) + } /> {formErrors.userType} - + Experience Level - setFormData({ ...formData, experienceLevel: e.target.value }) + setFormData({ + ...formData, + experienceLevel: e.target.value, + }) } /> @@ -1687,21 +753,22 @@ const ProfileUpdate: React.FC = () => { - + Location setFormData({ ...formData, location: e.target.value })} + onChange={(e) => + setFormData({ ...formData, location: e.target.value }) + } /> {formErrors.location} - {/* COMPLETED FROM YOUR TRUNCATION POINT */} - + Available From { - {/* PHONE & DRIVERS LICENSE */} - + Phone Number setFormData({ ...formData, phoneNumber: e.target.value })} + onChange={(e) => + setFormData({ ...formData, phoneNumber: e.target.value }) + } /> {formErrors.phoneNumber} - - Driver's License + + Driver’s License - setFormData({ ...formData, driversLicense: e.target.value }) + setFormData({ + ...formData, + driversLicense: e.target.value, + }) } /> @@ -1748,62 +819,43 @@ const ProfileUpdate: React.FC = () => { - {/* COMMENTS */} - - - Additional Comments - setFormData({ ...formData, comments: e.target.value })} - /> - - {formErrors.comments} - - - - - {/* PASSWORD UPDATE */} - - - Current Password - setFormData({ ...formData, password: e.target.value })} - /> - - - - New Password - - setFormData({ ...formData, newPassword: e.target.value }) - } - /> - - + + Comments + + setFormData({ ...formData, comments: e.target.value }) + } + /> + + {formErrors.comments} + + + + + New Password + + setFormData({ ...formData, newPassword: e.target.value }) + } + /> + - {/* SUBMIT BUTTON */} -
+
- - {/* BACK BUTTON */} -
- Back to Profile -
); From 1efb2e275bc40704d9e6467f93a66db8e2924fa7 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 15:19:40 -0500 Subject: [PATCH 13/16] save --- controllers/UserAPIRoutes.js | 132 ++++++++++------------------------- 1 file changed, 36 insertions(+), 96 deletions(-) diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index f10765a..cdc4ce0 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -48,127 +48,67 @@ router.get("/view/:id", (req, res) => { }); }); -// router.put("/update/:id", upload.single("profileImage"), async (req, res) => { -// try { -// // 1️⃣ Fetch the user -// const user = await db.User.findByPk(req.params.id); -// if (!user) { -// return res.status(404).send({ success: false, message: "User not found" }); -// } - -// // 2️⃣ Handle password update -// const updateData = { ...req.body }; - -// if (updateData.newPassword) { -// const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); -// updateData.password = hashedPassword; -// delete updateData.newPassword; -// } - -// // 3️⃣ Handle image upload (optional) -// let imageUrl = user.profileImage; // keep previous image by default - -// if (req.file) { -// try { -// const result = await cloudinary.uploader.upload(req.file.path, { -// folder: "users", -// public_id: `user_${user.id}_${Date.now()}`, -// transformation: [{ width: 500, height: 500, crop: "fill" }], -// }); - -// imageUrl = result.secure_url; -// } catch (err) { -// console.error("Cloudinary Upload Error:", err); -// return res.status(500).json({ -// success: false, -// message: "Image upload failed", -// }); -// } -// } - -// updateData.profileImage = imageUrl; - -// // 4️⃣ Update the user -// const [updatedRows] = await db.User.update(updateData, { -// where: { id: req.params.id }, -// }); - -// if (updatedRows === 0) { -// return res.status(404).send({ success: false, message: "No updates made" }); -// } - -// // 5️⃣ Fetch updated user -// const updatedUser = await db.User.findByPk(req.params.id); - -// res.status(200).send({ -// success: true, -// message: "User updated successfully", -// user: updatedUser, -// }); - -// } catch (error) { -// console.error("Error updating user:", error); -// res.status(500).send({ success: false, message: "Internal Server Error" }); -// } -// }); router.put("/update/:id", upload.single("profileImage"), async (req, res) => { try { + // 1️⃣ Fetch the user const user = await db.User.findByPk(req.params.id); if (!user) { - return res.status(404).json({ success: false, message: "User not found" }); + return res.status(404).send({ success: false, message: "User not found" }); + } + + // 2️⃣ Handle password update + const updateData = { ...req.body }; + + if (updateData.newPassword) { + const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); + updateData.password = hashedPassword; + delete updateData.newPassword; } - let cloudinaryImageUrl = user.profileImage; // keep old image if none uploaded + // 3️⃣ Handle image upload (optional) + let imageUrl = user.profileImage; // keep previous image by default - // 1️⃣ If frontend uploaded an image file, upload it to Cloudinary FIRST if (req.file) { try { - const uploadResult = await cloudinary.uploader.upload(req.file.path); - cloudinaryImageUrl = uploadResult.secure_url; + const result = await cloudinary.uploader.upload(req.file.path, { + folder: "users", + public_id: `user_${user.id}_${Date.now()}`, + transformation: [{ width: 500, height: 500, crop: "fill" }], + }); + + imageUrl = result.secure_url; } catch (err) { console.error("Cloudinary Upload Error:", err); return res.status(500).json({ success: false, message: "Image upload failed", - error: err, }); } } - // 2️⃣ Build update object (don’t overwrite fields that weren’t sent) - const updateData = { - email: req.body.email ?? user.email, - description: req.body.description ?? user.description, - userType: req.body.userType ?? user.userType, - experienceLevel: req.body.experienceLevel ?? user.experienceLevel, - location: req.body.location ?? user.location, - availableFrom: req.body.availableFrom ?? user.availableFrom, - phoneNumber: req.body.phoneNumber ?? user.phoneNumber, - driversLicense: req.body.driversLicense ?? user.driversLicense, - comments: req.body.comments ?? user.comments, - profileImage: cloudinaryImageUrl, // ⬅️ ALWAYS FINAL IMAGE HERE - }; + updateData.profileImage = imageUrl; - // 3️⃣ Handle password update - if (req.body.newPassword) { - const bcrypt = require("bcryptjs"); - const hashed = await bcrypt.hash(req.body.newPassword, 10); - updateData.password = hashed; + // 4️⃣ Update the user + const [updatedRows] = await db.User.update(updateData, { + where: { id: req.params.id }, + }); + + if (updatedRows === 0) { + return res.status(404).send({ success: false, message: "No updates made" }); } - // 4️⃣ Update DB - await user.update(updateData); + // 5️⃣ Fetch updated user + const updatedUser = await db.User.findByPk(req.params.id); - return res.status(200).json({ + res.status(200).send({ success: true, - message: "Profile updated successfully", - user, - image: cloudinaryImageUrl, + message: "User updated successfully", + user: updatedUser, }); - } catch (err) { - console.error("Profile Update Error:", err); - return res.status(500).json({ success: false, error: err }); + } catch (error) { + console.error("Error updating user:", error); + res.status(500).send({ success: false, message: "Internal Server Error" }); } }); From fb0002b4b325ae4b8c093cffcd94e6eb159170c4 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 15:25:23 -0500 Subject: [PATCH 14/16] update --- .../containers/User/Profile/ProfileUpdate.tsx | 773 ++++++++++++++---- 1 file changed, 620 insertions(+), 153 deletions(-) diff --git a/client/src/containers/User/Profile/ProfileUpdate.tsx b/client/src/containers/User/Profile/ProfileUpdate.tsx index bda31f6..d9bfc05 100644 --- a/client/src/containers/User/Profile/ProfileUpdate.tsx +++ b/client/src/containers/User/Profile/ProfileUpdate.tsx @@ -428,7 +428,442 @@ // export default ProfileUpdate; -import React, { useState, useEffect } from "react"; +// import React, { useState, useEffect } from "react"; +// import axios from "axios"; +// import { +// Button, +// Col, +// Form, +// Row, +// Alert, +// Card, +// Spinner, +// } from "react-bootstrap"; + +// type User = { +// id: string; +// email: string; +// password?: string; +// description: string; +// userType: string; +// experienceLevel: string; +// location: string; +// availableFrom: string; +// newPassword?: string; +// profileImage?: string; +// phoneNumber: string; +// driversLicense: string; +// comments: string; +// qrCode?: string; +// }; + +// const ServerPort = +// process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; + +// const CLOUDINARY_UPLOAD_URL = `https://api.cloudinary.com/v1_1/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload`; +// const CLOUDINARY_UPLOAD_PRESET = process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET; + +// const ProfileUpdate: React.FC = () => { +// const userId = localStorage.getItem("userId"); + +// const [formData, setFormData] = useState({ +// id: "", +// email: "", +// password: "", +// description: "", +// userType: "", +// experienceLevel: "", +// location: "", +// availableFrom: "", +// newPassword: "", +// profileImage: "", +// phoneNumber: "", +// driversLicense: "", +// comments: "", +// qrCode: "", +// }); + +// const [imagePreview, setImagePreview] = useState(null); +// const [qrCodeImage, setQrCodeImage] = useState(null); +// const [formErrors, setFormErrors] = useState>({}); +// const [loading, setLoading] = useState(false); +// const [isSubmitting, setIsSubmitting] = useState(false); +// const [error, setError] = useState(null); +// const [successMessage, setSuccessMessage] = useState(null); + +// // ------------------------- +// // LOAD USER PROFILE +// // ------------------------- +// useEffect(() => { +// const fetchUserData = async () => { +// setLoading(true); + +// try { +// const token = localStorage.getItem("token"); +// if (!token) return setError("No token found."); + +// const response = await axios.get( +// `${ServerPort}/api/user/view/${userId}`, +// { headers: { Authorization: `Bearer ${token}` } } +// ); + +// const user = response.data; + +// setFormData({ +// ...user, +// availableFrom: user.availableFrom?.split("T")[0] || "", +// password: "", +// newPassword: "", +// }); + +// setImagePreview(user.profileImage || null); +// setQrCodeImage(user.qrCode || null); +// } catch (err) { +// handleApiError(err); +// } + +// setLoading(false); +// }; + +// if (userId) fetchUserData(); +// }, [userId]); + +// // ------------------------- +// // IMAGE UPLOAD TO CLOUDINARY +// // ------------------------- +// const handleImageUpload = async (file: File) => { +// setLoading(true); + +// try { +// const data = new FormData(); +// data.append("file", file); +// data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!); + +// const uploadRes = await axios.post(CLOUDINARY_UPLOAD_URL, data); +// const secureUrl = uploadRes.data.secure_url; + +// setFormData((prev) => ({ ...prev, profileImage: secureUrl })); +// setImagePreview(secureUrl); +// } catch (err) { +// setError("Image upload failed. Try again."); +// } + +// setLoading(false); +// }; + +// // ------------------------- +// // FORM VALIDATION +// // ------------------------- +// const validateForm = () => { +// const errors: Record = {}; +// const required = [ +// "email", +// "description", +// "userType", +// "experienceLevel", +// "location", +// "availableFrom", +// "phoneNumber", +// "driversLicense", +// "comments", +// ]; + +// required.forEach((field) => { +// // @ts-ignore +// if (!formData[field]) errors[field] = "This field is required"; +// }); + +// if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) +// errors.email = "Invalid email"; + +// setFormErrors(errors); + +// return Object.keys(errors).length === 0; +// }; + +// // ------------------------- +// // UPDATE USER +// // ------------------------- +// const handleUserUpdate = async (e: React.FormEvent) => { +// e.preventDefault(); +// if (!validateForm()) return; + +// setLoading(true); +// setIsSubmitting(true); +// setError(null); +// setSuccessMessage(null); + +// try { +// const token = localStorage.getItem("token"); + +// const payload = { ...formData }; +// if (!formData.newPassword) delete payload.password; + +// const res = await axios.put( +// `${ServerPort}/api/user/update/${userId}`, +// payload, +// { +// headers: { +// Authorization: `Bearer ${token}`, +// "Content-Type": "application/json", +// }, +// } +// ); + +// if (res.status === 200) { +// setSuccessMessage("Profile successfully updated!"); +// setQrCodeImage(res.data.qrCode || qrCodeImage); +// setFormData((prev) => ({ ...prev, newPassword: "" })); +// } +// } catch (err) { +// handleApiError(err); +// } + +// setIsSubmitting(false); +// setLoading(false); +// }; + +// // ------------------------- +// // ERROR HANDLER +// // ------------------------- +// const handleApiError = (error: any) => { +// if (axios.isAxiosError(error)) { +// setError(error.response?.data?.message || "Something went wrong."); +// } else { +// setError("An unexpected error occurred."); +// } +// }; + +// // ------------------------- +// // UI RENDER +// // ------------------------- +// return ( +// +//

Update Profile

+ +// {loading && ( +// +// )} +// {error && {error}} +// {successMessage && {successMessage}} + +//
+// {/* PROFILE IMAGE UPLOAD */} +//
+// Profile + +// ) => { +// const file = e.target.files?.[0]; +// if (file) handleImageUpload(file); +// }} +// /> +//
+ +// {/* QR CODE */} +// {qrCodeImage && ( +//
+//
Your QR Code
+// QR Code +//
+// )} + +// {/* FORM ROWS */} +// +// +// Email +// +// setFormData({ ...formData, email: e.target.value }) +// } +// /> +// +// {formErrors.email} +// +// + +// +// Description +// +// setFormData({ ...formData, description: e.target.value }) +// } +// /> +// +// {formErrors.description} +// +// +// + +// +// +// User Type +// +// setFormData({ ...formData, userType: e.target.value }) +// } +// /> +// +// {formErrors.userType} +// +// + +// +// Experience Level +// +// setFormData({ +// ...formData, +// experienceLevel: e.target.value, +// }) +// } +// /> +// +// {formErrors.experienceLevel} +// +// +// + +// +// +// Location +// +// setFormData({ ...formData, location: e.target.value }) +// } +// /> +// +// {formErrors.location} +// +// + +// +// Available From +// +// setFormData({ ...formData, availableFrom: e.target.value }) +// } +// /> +// +// {formErrors.availableFrom} +// +// +// + +// +// +// Phone Number +// +// setFormData({ ...formData, phoneNumber: e.target.value }) +// } +// /> +// +// {formErrors.phoneNumber} +// +// + +// +// Driver’s License +// +// setFormData({ +// ...formData, +// driversLicense: e.target.value, +// }) +// } +// /> +// +// {formErrors.driversLicense} +// +// +// + +// +// Comments +// +// setFormData({ ...formData, comments: e.target.value }) +// } +// /> +// +// {formErrors.comments} +// +// + +// +// New Password +// +// setFormData({ ...formData, newPassword: e.target.value }) +// } +// /> +// + +//
+// +//
+//
+//
+// ); +// }; + +// export default ProfileUpdate; + +import React, { useEffect, useState } from "react"; import axios from "axios"; import { Button, @@ -439,6 +874,7 @@ import { Card, Spinner, } from "react-bootstrap"; +import { Link } from "react-router-dom"; type User = { id: string; @@ -460,11 +896,8 @@ type User = { const ServerPort = process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; -const CLOUDINARY_UPLOAD_URL = `https://api.cloudinary.com/v1_1/${process.env.REACT_APP_CLOUDINARY_CLOUD_NAME}/image/upload`; -const CLOUDINARY_UPLOAD_PRESET = process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET; - const ProfileUpdate: React.FC = () => { - const userId = localStorage.getItem("userId"); + const userId = localStorage.getItem("userId") || ""; const [formData, setFormData] = useState({ id: "", @@ -483,6 +916,7 @@ const ProfileUpdate: React.FC = () => { qrCode: "", }); + const [selectedFile, setSelectedFile] = useState(null); const [imagePreview, setImagePreview] = useState(null); const [qrCodeImage, setQrCodeImage] = useState(null); const [formErrors, setFormErrors] = useState>({}); @@ -491,69 +925,71 @@ const ProfileUpdate: React.FC = () => { const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); - // ------------------------- - // LOAD USER PROFILE - // ------------------------- + // load user profile useEffect(() => { const fetchUserData = async () => { setLoading(true); - try { const token = localStorage.getItem("token"); - if (!token) return setError("No token found."); - - const response = await axios.get( - `${ServerPort}/api/user/view/${userId}`, - { headers: { Authorization: `Bearer ${token}` } } - ); - - const user = response.data; + if (!token) { + setError("No token found."); + setLoading(false); + return; + } - setFormData({ - ...user, - availableFrom: user.availableFrom?.split("T")[0] || "", - password: "", - newPassword: "", + const res = await axios.get(`${ServerPort}/api/user/view/${userId}`, { + headers: { Authorization: `Bearer ${token}` }, }); - setImagePreview(user.profileImage || null); - setQrCodeImage(user.qrCode || null); + if (res.status === 200) { + const user = res.data; + setFormData({ + ...user, + availableFrom: user.availableFrom?.split("T")[0] || "", + password: "", + newPassword: "", + }); + setImagePreview(user.profileImage || null); + setQrCodeImage(user.qrPNG || user.qrCode || null); + } else { + setError("Failed to fetch user data."); + } } catch (err) { handleApiError(err); + } finally { + setLoading(false); } - - setLoading(false); }; if (userId) fetchUserData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [userId]); - // ------------------------- - // IMAGE UPLOAD TO CLOUDINARY - // ------------------------- - const handleImageUpload = async (file: File) => { - setLoading(true); - - try { - const data = new FormData(); - data.append("file", file); - data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!); - - const uploadRes = await axios.post(CLOUDINARY_UPLOAD_URL, data); - const secureUrl = uploadRes.data.secure_url; - - setFormData((prev) => ({ ...prev, profileImage: secureUrl })); - setImagePreview(secureUrl); - } catch (err) { - setError("Image upload failed. Try again."); + // cleanup object URL when component unmounts / file changes + useEffect(() => { + return () => { + if (imagePreview && imagePreview.startsWith("blob:")) { + URL.revokeObjectURL(imagePreview); + } + }; + }, [imagePreview]); + + // handle native file input change (use plain input to avoid bootstrap issues) + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] ?? null; + if (file) { + // revoke previous blob url if any + if (imagePreview && imagePreview.startsWith("blob:")) { + URL.revokeObjectURL(imagePreview); + } + setSelectedFile(file); + setImagePreview(URL.createObjectURL(file)); + // don't mutate formData.profileImage here — backend will return permanent URL + } else { + setSelectedFile(null); } - - setLoading(false); }; - // ------------------------- - // FORM VALIDATION - // ------------------------- const validateForm = () => { const errors: Record = {}; const required = [ @@ -566,77 +1002,124 @@ const ProfileUpdate: React.FC = () => { "phoneNumber", "driversLicense", "comments", - ]; + ] as const; - required.forEach((field) => { + for (const field of required) { // @ts-ignore if (!formData[field]) errors[field] = "This field is required"; - }); + } - if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) + if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) { errors.email = "Invalid email"; + } setFormErrors(errors); - return Object.keys(errors).length === 0; }; - // ------------------------- - // UPDATE USER - // ------------------------- + // Submit: if selectedFile exists -> send multipart/form-data to backend, + // otherwise send JSON. Backend should accept multipart and handle saving to Cloudinary. const handleUserUpdate = async (e: React.FormEvent) => { e.preventDefault(); + setError(null); + setSuccessMessage(null); + if (!validateForm()) return; setLoading(true); setIsSubmitting(true); - setError(null); - setSuccessMessage(null); try { const token = localStorage.getItem("token"); + if (!token) throw new Error("No token - please sign in."); + + if (selectedFile) { + // send multipart to backend; backend must use multer to accept `profileImage` + const form = new FormData(); + + // append scalar fields + Object.entries(formData).forEach(([k, v]) => { + if (v !== undefined && v !== null) form.append(k, String(v)); + }); - const payload = { ...formData }; - if (!formData.newPassword) delete payload.password; - - const res = await axios.put( - `${ServerPort}/api/user/update/${userId}`, - payload, - { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, + form.append("profileImage", selectedFile); + + const res = await axios.put( + `${ServerPort}/api/user/update/${userId}`, + form, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "multipart/form-data", + }, + } + ); + + if (res.status === 200) { + const updatedUser = res.data.user ?? res.data; + setFormData((prev) => ({ + ...prev, + ...updatedUser, + newPassword: "", + })); + // server should return permanent image URL (profileImage) + setImagePreview(updatedUser.profileImage || imagePreview); + setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); + setSuccessMessage("Profile updated and image uploaded."); + setSelectedFile(null); + } else { + setError("Unexpected server response when uploading image."); } - ); + } else { + // no file selected — send JSON update + const payload: Partial = { ...formData }; + if (!formData.newPassword) delete payload.password; + + const res = await axios.put( + `${ServerPort}/api/user/update/${userId}`, + payload, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); - if (res.status === 200) { - setSuccessMessage("Profile successfully updated!"); - setQrCodeImage(res.data.qrCode || qrCodeImage); - setFormData((prev) => ({ ...prev, newPassword: "" })); + if (res.status === 200) { + const updatedUser = res.data.user ?? res.data; + setFormData((prev) => ({ + ...prev, + ...updatedUser, + newPassword: "", + })); + setImagePreview(updatedUser.profileImage || imagePreview); + setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); + setSuccessMessage("Profile updated."); + } else { + setError("Unexpected server response when updating profile."); + } } } catch (err) { handleApiError(err); + } finally { + setIsSubmitting(false); + setLoading(false); } - - setIsSubmitting(false); - setLoading(false); }; - // ------------------------- - // ERROR HANDLER - // ------------------------- - const handleApiError = (error: any) => { - if (axios.isAxiosError(error)) { - setError(error.response?.data?.message || "Something went wrong."); + const handleApiError = (err: any) => { + if (axios.isAxiosError(err)) { + const msg = + (err.response && (err.response.data?.message || err.response.data)) || + err.message || + "Server error"; + setError(String(msg)); } else { - setError("An unexpected error occurred."); + setError(String(err || "Unknown error")); } }; - // ------------------------- - // UI RENDER - // ------------------------- return (

Update Profile

@@ -647,12 +1130,13 @@ const ProfileUpdate: React.FC = () => { {error && {error}} {successMessage && {successMessage}} -
- {/* PROFILE IMAGE UPLOAD */} + + {/* IMAGE PREVIEW + NATIVE FILE INPUT (avoid Form.Control for file) */}
Profile { border: "3px solid #ddd", }} /> - - ) => { - const file = e.target.files?.[0]; - if (file) handleImageUpload(file); - }} - /> +
+ + {selectedFile && ( +
+ Selected: {selectedFile.name} +
+ )} +
{/* QR CODE */} {qrCodeImage && (
Your QR Code
- QR Code + QR Code
)} - {/* FORM ROWS */} + {/* FORM FIELDS */} - + Email - setFormData({ ...formData, email: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, email: e.target.value })} /> {formErrors.email} - + Description - setFormData({ ...formData, description: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, description: e.target.value })} /> {formErrors.description} @@ -718,32 +1207,27 @@ const ProfileUpdate: React.FC = () => { - + User Type - setFormData({ ...formData, userType: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, userType: e.target.value })} /> {formErrors.userType} - + Experience Level - setFormData({ - ...formData, - experienceLevel: e.target.value, - }) + setFormData({ ...formData, experienceLevel: e.target.value }) } /> @@ -753,30 +1237,26 @@ const ProfileUpdate: React.FC = () => { - + Location - setFormData({ ...formData, location: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, location: e.target.value })} /> {formErrors.location} - + Available From - setFormData({ ...formData, availableFrom: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, availableFrom: e.target.value })} /> {formErrors.availableFrom} @@ -785,33 +1265,26 @@ const ProfileUpdate: React.FC = () => { - + Phone Number - setFormData({ ...formData, phoneNumber: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, phoneNumber: e.target.value })} /> {formErrors.phoneNumber} - + Driver’s License - setFormData({ - ...formData, - driversLicense: e.target.value, - }) - } + onChange={(e) => setFormData({ ...formData, driversLicense: e.target.value })} /> {formErrors.driversLicense} @@ -819,42 +1292,36 @@ const ProfileUpdate: React.FC = () => { - + Comments - setFormData({ ...formData, comments: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, comments: e.target.value })} /> {formErrors.comments} - + New Password - setFormData({ ...formData, newPassword: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, newPassword: e.target.value })} /> -
- + + +
From 90b75ba3de4d8a1b5707fbace2938983f3b1b8a3 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 15:50:59 -0500 Subject: [PATCH 15/16] save --- .../containers/User/Profile/ProfileUpdate.tsx | 794 ++++++++++++------ controllers/UserAPIRoutes.js | 121 ++- 2 files changed, 605 insertions(+), 310 deletions(-) diff --git a/client/src/containers/User/Profile/ProfileUpdate.tsx b/client/src/containers/User/Profile/ProfileUpdate.tsx index d9bfc05..c81d8ba 100644 --- a/client/src/containers/User/Profile/ProfileUpdate.tsx +++ b/client/src/containers/User/Profile/ProfileUpdate.tsx @@ -863,29 +863,487 @@ // export default ProfileUpdate; +// import React, { useEffect, useState } from "react"; +// import axios from "axios"; +// import { +// Button, +// Col, +// Form, +// Row, +// Alert, +// Card, +// Spinner, +// } from "react-bootstrap"; +// import { Link } from "react-router-dom"; + +// type User = { +// id: string; +// email: string; +// password?: string; +// description: string; +// userType: string; +// experienceLevel: string; +// location: string; +// availableFrom: string; +// newPassword?: string; +// profileImage?: string; +// phoneNumber: string; +// driversLicense: string; +// comments: string; +// qrCode?: string; +// }; + +// const ServerPort = +// process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; + +// const ProfileUpdate: React.FC = () => { +// const userId = localStorage.getItem("userId") || ""; + +// const [formData, setFormData] = useState({ +// id: "", +// email: "", +// password: "", +// description: "", +// userType: "", +// experienceLevel: "", +// location: "", +// availableFrom: "", +// newPassword: "", +// profileImage: "", +// phoneNumber: "", +// driversLicense: "", +// comments: "", +// qrCode: "", +// }); + +// const [selectedFile, setSelectedFile] = useState(null); +// const [imagePreview, setImagePreview] = useState(null); +// const [qrCodeImage, setQrCodeImage] = useState(null); +// const [formErrors, setFormErrors] = useState>({}); +// const [loading, setLoading] = useState(false); +// const [isSubmitting, setIsSubmitting] = useState(false); +// const [error, setError] = useState(null); +// const [successMessage, setSuccessMessage] = useState(null); + +// // load user profile +// useEffect(() => { +// const fetchUserData = async () => { +// setLoading(true); +// try { +// const token = localStorage.getItem("token"); +// if (!token) { +// setError("No token found."); +// setLoading(false); +// return; +// } + +// const res = await axios.get(`${ServerPort}/api/user/view/${userId}`, { +// headers: { Authorization: `Bearer ${token}` }, +// }); + +// if (res.status === 200) { +// const user = res.data; +// setFormData({ +// ...user, +// availableFrom: user.availableFrom?.split("T")[0] || "", +// password: "", +// newPassword: "", +// }); +// setImagePreview(user.profileImage || null); +// setQrCodeImage(user.qrPNG || user.qrCode || null); +// } else { +// setError("Failed to fetch user data."); +// } +// } catch (err) { +// handleApiError(err); +// } finally { +// setLoading(false); +// } +// }; + +// if (userId) fetchUserData(); +// // eslint-disable-next-line react-hooks/exhaustive-deps +// }, [userId]); + +// // cleanup object URL when component unmounts / file changes +// useEffect(() => { +// return () => { +// if (imagePreview && imagePreview.startsWith("blob:")) { +// URL.revokeObjectURL(imagePreview); +// } +// }; +// }, [imagePreview]); + +// // handle native file input change (use plain input to avoid bootstrap issues) +// const handleFileChange = (e: React.ChangeEvent) => { +// const file = e.target.files?.[0] ?? null; +// if (file) { +// // revoke previous blob url if any +// if (imagePreview && imagePreview.startsWith("blob:")) { +// URL.revokeObjectURL(imagePreview); +// } +// setSelectedFile(file); +// setImagePreview(URL.createObjectURL(file)); +// // don't mutate formData.profileImage here — backend will return permanent URL +// } else { +// setSelectedFile(null); +// } +// }; + +// const validateForm = () => { +// const errors: Record = {}; +// const required = [ +// "email", +// "description", +// "userType", +// "experienceLevel", +// "location", +// "availableFrom", +// "phoneNumber", +// "driversLicense", +// "comments", +// ] as const; + +// for (const field of required) { +// // @ts-ignore +// if (!formData[field]) errors[field] = "This field is required"; +// } + +// if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) { +// errors.email = "Invalid email"; +// } + +// setFormErrors(errors); +// return Object.keys(errors).length === 0; +// }; + +// // Submit: if selectedFile exists -> send multipart/form-data to backend, +// // otherwise send JSON. Backend should accept multipart and handle saving to Cloudinary. +// const handleUserUpdate = async (e: React.FormEvent) => { +// e.preventDefault(); +// setError(null); +// setSuccessMessage(null); + +// if (!validateForm()) return; + +// setLoading(true); +// setIsSubmitting(true); + +// try { +// const token = localStorage.getItem("token"); +// if (!token) throw new Error("No token - please sign in."); + +// if (selectedFile) { +// // send multipart to backend; backend must use multer to accept `profileImage` +// const form = new FormData(); + +// // append scalar fields +// Object.entries(formData).forEach(([k, v]) => { +// if (v !== undefined && v !== null) form.append(k, String(v)); +// }); + +// form.append("profileImage", selectedFile); + +// const res = await axios.put( +// `${ServerPort}/api/user/update/${userId}`, +// form, +// { +// headers: { +// Authorization: `Bearer ${token}`, +// "Content-Type": "multipart/form-data", +// }, +// } +// ); + +// if (res.status === 200) { +// const updatedUser = res.data.user ?? res.data; +// setFormData((prev) => ({ +// ...prev, +// ...updatedUser, +// newPassword: "", +// })); +// // server should return permanent image URL (profileImage) +// setImagePreview(updatedUser.profileImage || imagePreview); +// setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); +// setSuccessMessage("Profile updated and image uploaded."); +// setSelectedFile(null); +// } else { +// setError("Unexpected server response when uploading image."); +// } +// } else { +// // no file selected — send JSON update +// const payload: Partial = { ...formData }; +// if (!formData.newPassword) delete payload.password; + +// const res = await axios.put( +// `${ServerPort}/api/user/update/${userId}`, +// payload, +// { +// headers: { +// Authorization: `Bearer ${token}`, +// "Content-Type": "application/json", +// }, +// } +// ); + +// if (res.status === 200) { +// const updatedUser = res.data.user ?? res.data; +// setFormData((prev) => ({ +// ...prev, +// ...updatedUser, +// newPassword: "", +// })); +// setImagePreview(updatedUser.profileImage || imagePreview); +// setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); +// setSuccessMessage("Profile updated."); +// } else { +// setError("Unexpected server response when updating profile."); +// } +// } +// } catch (err) { +// handleApiError(err); +// } finally { +// setIsSubmitting(false); +// setLoading(false); +// } +// }; + +// const handleApiError = (err: any) => { +// if (axios.isAxiosError(err)) { +// const msg = +// (err.response && (err.response.data?.message || err.response.data)) || +// err.message || +// "Server error"; +// setError(String(msg)); +// } else { +// setError(String(err || "Unknown error")); +// } +// }; + +// return ( +// +//

Update Profile

+ +// {loading && ( +// +// )} +// {error && {error}} +// {successMessage && {successMessage}} + +//
+// {/* IMAGE PREVIEW + NATIVE FILE INPUT (avoid Form.Control for file) */} +//
+// Profile +//
+// +// {selectedFile && ( +//
+// Selected: {selectedFile.name} +//
+// )} +//
+//
+ +// {/* QR CODE */} +// {qrCodeImage && ( +//
+//
Your QR Code
+// QR Code +//
+// )} + +// {/* FORM FIELDS */} +// +// +// Email +// setFormData({ ...formData, email: e.target.value })} +// /> +// +// {formErrors.email} +// +// + +// +// Description +// setFormData({ ...formData, description: e.target.value })} +// /> +// +// {formErrors.description} +// +// +// + +// +// +// User Type +// setFormData({ ...formData, userType: e.target.value })} +// /> +// +// {formErrors.userType} +// +// + +// +// Experience Level +// +// setFormData({ ...formData, experienceLevel: e.target.value }) +// } +// /> +// +// {formErrors.experienceLevel} +// +// +// + +// +// +// Location +// setFormData({ ...formData, location: e.target.value })} +// /> +// +// {formErrors.location} +// +// + +// +// Available From +// setFormData({ ...formData, availableFrom: e.target.value })} +// /> +// +// {formErrors.availableFrom} +// +// +// + +// +// +// Phone Number +// setFormData({ ...formData, phoneNumber: e.target.value })} +// /> +// +// {formErrors.phoneNumber} +// +// + +// +// Driver’s License +// setFormData({ ...formData, driversLicense: e.target.value })} +// /> +// +// {formErrors.driversLicense} +// +// +// + +// +// Comments +// setFormData({ ...formData, comments: e.target.value })} +// /> +// +// {formErrors.comments} +// +// + +// +// New Password +// setFormData({ ...formData, newPassword: e.target.value })} +// /> +// + +//
+// +// +// +// +//
+//
+//
+// ); +// }; + +// export default ProfileUpdate; import React, { useEffect, useState } from "react"; import axios from "axios"; -import { - Button, - Col, - Form, - Row, - Alert, - Card, - Spinner, -} from "react-bootstrap"; +import { Button, Col, Form, Row, Alert, Card, Spinner } from "react-bootstrap"; import { Link } from "react-router-dom"; type User = { id: string; email: string; password?: string; + newPassword?: string; description: string; userType: string; experienceLevel: string; location: string; availableFrom: string; - newPassword?: string; profileImage?: string; phoneNumber: string; driversLicense: string; @@ -893,8 +1351,7 @@ type User = { qrCode?: string; }; -const ServerPort = - process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; +const ServerPort = process.env.REACT_APP_SOCKET_IO_CLIENT_PORT || "http://localhost:3001"; const ProfileUpdate: React.FC = () => { const userId = localStorage.getItem("userId") || ""; @@ -903,12 +1360,12 @@ const ProfileUpdate: React.FC = () => { id: "", email: "", password: "", + newPassword: "", description: "", userType: "", experienceLevel: "", location: "", availableFrom: "", - newPassword: "", profileImage: "", phoneNumber: "", driversLicense: "", @@ -916,16 +1373,15 @@ const ProfileUpdate: React.FC = () => { qrCode: "", }); - const [selectedFile, setSelectedFile] = useState(null); const [imagePreview, setImagePreview] = useState(null); - const [qrCodeImage, setQrCodeImage] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); const [formErrors, setFormErrors] = useState>({}); const [loading, setLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); - // load user profile + // Load user profile useEffect(() => { const fetchUserData = async () => { setLoading(true); @@ -950,7 +1406,6 @@ const ProfileUpdate: React.FC = () => { newPassword: "", }); setImagePreview(user.profileImage || null); - setQrCodeImage(user.qrPNG || user.qrCode || null); } else { setError("Failed to fetch user data."); } @@ -962,37 +1417,32 @@ const ProfileUpdate: React.FC = () => { }; if (userId) fetchUserData(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [userId]); - // cleanup object URL when component unmounts / file changes - useEffect(() => { - return () => { - if (imagePreview && imagePreview.startsWith("blob:")) { - URL.revokeObjectURL(imagePreview); - } - }; - }, [imagePreview]); - - // handle native file input change (use plain input to avoid bootstrap issues) + // Handle file input change and convert to preview const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] ?? null; if (file) { - // revoke previous blob url if any - if (imagePreview && imagePreview.startsWith("blob:")) { - URL.revokeObjectURL(imagePreview); - } setSelectedFile(file); setImagePreview(URL.createObjectURL(file)); - // don't mutate formData.profileImage here — backend will return permanent URL } else { setSelectedFile(null); } }; + // Convert image file to base64 + const fileToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = (err) => reject(err); + }); + }; + const validateForm = () => { const errors: Record = {}; - const required = [ + const requiredFields: (keyof User)[] = [ "email", "description", "userType", @@ -1002,12 +1452,11 @@ const ProfileUpdate: React.FC = () => { "phoneNumber", "driversLicense", "comments", - ] as const; + ]; - for (const field of required) { - // @ts-ignore + requiredFields.forEach((field) => { if (!formData[field]) errors[field] = "This field is required"; - } + }); if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) { errors.email = "Invalid email"; @@ -1017,8 +1466,7 @@ const ProfileUpdate: React.FC = () => { return Object.keys(errors).length === 0; }; - // Submit: if selectedFile exists -> send multipart/form-data to backend, - // otherwise send JSON. Backend should accept multipart and handle saving to Cloudinary. + // Submit handler const handleUserUpdate = async (e: React.FormEvent) => { e.preventDefault(); setError(null); @@ -1031,80 +1479,38 @@ const ProfileUpdate: React.FC = () => { try { const token = localStorage.getItem("token"); - if (!token) throw new Error("No token - please sign in."); - - if (selectedFile) { - // send multipart to backend; backend must use multer to accept `profileImage` - const form = new FormData(); + if (!token) throw new Error("No token found"); - // append scalar fields - Object.entries(formData).forEach(([k, v]) => { - if (v !== undefined && v !== null) form.append(k, String(v)); - }); + const payload: any = { ...formData }; - form.append("profileImage", selectedFile); + // Remove password field if no new password + if (!formData.newPassword) delete payload.password; - const res = await axios.put( - `${ServerPort}/api/user/update/${userId}`, - form, - { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "multipart/form-data", - }, - } - ); + // Convert selected file to base64 + if (selectedFile) { + payload.profileImage = await fileToBase64(selectedFile); + } - if (res.status === 200) { - const updatedUser = res.data.user ?? res.data; - setFormData((prev) => ({ - ...prev, - ...updatedUser, - newPassword: "", - })); - // server should return permanent image URL (profileImage) - setImagePreview(updatedUser.profileImage || imagePreview); - setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); - setSuccessMessage("Profile updated and image uploaded."); - setSelectedFile(null); - } else { - setError("Unexpected server response when uploading image."); - } + const res = await axios.put( + `${ServerPort}/api/user/update/${userId}`, + payload, + { headers: { Authorization: `Bearer ${token}` } } + ); + + if (res.status === 200) { + const updatedUser = res.data.user ?? res.data; + setFormData((prev) => ({ ...prev, ...updatedUser, newPassword: "" })); + setImagePreview(updatedUser.profileImage || imagePreview); + setSelectedFile(null); + setSuccessMessage("Profile updated and image uploaded."); } else { - // no file selected — send JSON update - const payload: Partial = { ...formData }; - if (!formData.newPassword) delete payload.password; - - const res = await axios.put( - `${ServerPort}/api/user/update/${userId}`, - payload, - { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - } - ); - - if (res.status === 200) { - const updatedUser = res.data.user ?? res.data; - setFormData((prev) => ({ - ...prev, - ...updatedUser, - newPassword: "", - })); - setImagePreview(updatedUser.profileImage || imagePreview); - setQrCodeImage(updatedUser.qrPNG || updatedUser.qrCode || qrCodeImage); - setSuccessMessage("Profile updated."); - } else { - setError("Unexpected server response when updating profile."); - } + setError("Unexpected server response."); } } catch (err) { handleApiError(err); } finally { - setIsSubmitting(false); setLoading(false); + setIsSubmitting(false); } }; @@ -1124,60 +1530,25 @@ const ProfileUpdate: React.FC = () => {

Update Profile

- {loading && ( - - )} + {loading && } {error && {error}} {successMessage && {successMessage}} -
- {/* IMAGE PREVIEW + NATIVE FILE INPUT (avoid Form.Control for file) */} + + {/* Profile Image */}
Profile
- - {selectedFile && ( -
- Selected: {selectedFile.name} -
- )} + + {selectedFile &&
{selectedFile.name}
}
- {/* QR CODE */} - {qrCodeImage && ( -
-
Your QR Code
- QR Code -
- )} - - {/* FORM FIELDS */} + {/* Form Fields */} Email @@ -1187,9 +1558,7 @@ const ProfileUpdate: React.FC = () => { isInvalid={!!formErrors.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} /> - - {formErrors.email} - + {formErrors.email} @@ -1200,129 +1569,14 @@ const ProfileUpdate: React.FC = () => { isInvalid={!!formErrors.description} onChange={(e) => setFormData({ ...formData, description: e.target.value })} /> - - {formErrors.description} - - - - - - - User Type - setFormData({ ...formData, userType: e.target.value })} - /> - - {formErrors.userType} - - - - - Experience Level - - setFormData({ ...formData, experienceLevel: e.target.value }) - } - /> - - {formErrors.experienceLevel} - - - - - - - Location - setFormData({ ...formData, location: e.target.value })} - /> - - {formErrors.location} - - - - - Available From - setFormData({ ...formData, availableFrom: e.target.value })} - /> - - {formErrors.availableFrom} - + {formErrors.description} - - - Phone Number - setFormData({ ...formData, phoneNumber: e.target.value })} - /> - - {formErrors.phoneNumber} - - - - - Driver’s License - setFormData({ ...formData, driversLicense: e.target.value })} - /> - - {formErrors.driversLicense} - - - - - - Comments - setFormData({ ...formData, comments: e.target.value })} - /> - - {formErrors.comments} - - - - - New Password - setFormData({ ...formData, newPassword: e.target.value })} - /> - - -
- - - - -
+ {/* Add other fields similarly... */} +
); diff --git a/controllers/UserAPIRoutes.js b/controllers/UserAPIRoutes.js index cdc4ce0..9ff6ddc 100644 --- a/controllers/UserAPIRoutes.js +++ b/controllers/UserAPIRoutes.js @@ -48,67 +48,108 @@ router.get("/view/:id", (req, res) => { }); }); -router.put("/update/:id", upload.single("profileImage"), async (req, res) => { +// router.put("/update/:id", upload.single("profileImage"), async (req, res) => { +// try { +// // 1️⃣ Fetch the user +// const user = await db.User.findByPk(req.params.id); +// if (!user) { +// return res.status(404).send({ success: false, message: "User not found" }); +// } + +// // 2️⃣ Handle password update +// const updateData = { ...req.body }; + +// if (updateData.newPassword) { +// const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); +// updateData.password = hashedPassword; +// delete updateData.newPassword; +// } + +// // 3️⃣ Handle image upload (optional) +// let imageUrl = user.profileImage; // keep previous image by default + +// if (req.file) { +// try { +// const result = await cloudinary.uploader.upload(req.file.path, { +// folder: "users", +// public_id: `user_${user.id}_${Date.now()}`, +// transformation: [{ width: 500, height: 500, crop: "fill" }], +// }); + +// imageUrl = result.secure_url; +// } catch (err) { +// console.error("Cloudinary Upload Error:", err); +// return res.status(500).json({ +// success: false, +// message: "Image upload failed", +// }); +// } +// } + +// updateData.profileImage = imageUrl; + +// // 4️⃣ Update the user +// const [updatedRows] = await db.User.update(updateData, { +// where: { id: req.params.id }, +// }); + +// if (updatedRows === 0) { +// return res.status(404).send({ success: false, message: "No updates made" }); +// } + +// // 5️⃣ Fetch updated user +// const updatedUser = await db.User.findByPk(req.params.id); + +// res.status(200).send({ +// success: true, +// message: "User updated successfully", +// user: updatedUser, +// }); + +// } catch (error) { +// console.error("Error updating user:", error); +// res.status(500).send({ success: false, message: "Internal Server Error" }); +// } +// }); +router.put("/update/:id", async (req, res) => { try { - // 1️⃣ Fetch the user const user = await db.User.findByPk(req.params.id); - if (!user) { - return res.status(404).send({ success: false, message: "User not found" }); - } + if (!user) return res.status(404).json({ success: false, message: "User not found" }); - // 2️⃣ Handle password update const updateData = { ...req.body }; + // Handle new password if (updateData.newPassword) { - const hashedPassword = await bcrypt.hash(updateData.newPassword, 10); - updateData.password = hashedPassword; + const hashed = await bcrypt.hash(updateData.newPassword, 10); + updateData.password = hashed; delete updateData.newPassword; } - // 3️⃣ Handle image upload (optional) - let imageUrl = user.profileImage; // keep previous image by default - - if (req.file) { + // Handle profile image upload to Cloudinary + if (updateData.profileImage) { try { - const result = await cloudinary.uploader.upload(req.file.path, { + const result = await cloudinary.uploader.upload(updateData.profileImage, { folder: "users", public_id: `user_${user.id}_${Date.now()}`, transformation: [{ width: 500, height: 500, crop: "fill" }], }); - - imageUrl = result.secure_url; + updateData.profileImage = result.secure_url; } catch (err) { - console.error("Cloudinary Upload Error:", err); - return res.status(500).json({ - success: false, - message: "Image upload failed", - }); + console.error("Cloudinary upload failed:", err); + return res.status(500).json({ success: false, message: "Image upload failed" }); } } - updateData.profileImage = imageUrl; + // Update user + const [updatedRows] = await db.User.update(updateData, { where: { id: req.params.id } }); + if (updatedRows === 0) return res.status(404).json({ success: false, message: "No updates made" }); - // 4️⃣ Update the user - const [updatedRows] = await db.User.update(updateData, { - where: { id: req.params.id }, - }); - - if (updatedRows === 0) { - return res.status(404).send({ success: false, message: "No updates made" }); - } - - // 5️⃣ Fetch updated user const updatedUser = await db.User.findByPk(req.params.id); + res.status(200).json({ success: true, message: "User updated", user: updatedUser }); - res.status(200).send({ - success: true, - message: "User updated successfully", - user: updatedUser, - }); - - } catch (error) { - console.error("Error updating user:", error); - res.status(500).send({ success: false, message: "Internal Server Error" }); + } catch (err) { + console.error("Error updating user:", err); + res.status(500).json({ success: false, message: "Internal server error" }); } }); From 5fbe8ff1a23ebdb8e05ce48b2b13cc4f67d2fa70 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 28 Nov 2025 15:56:08 -0500 Subject: [PATCH 16/16] updates --- .../containers/User/Profile/ProfileUpdate.tsx | 118 +++++++++++++++--- 1 file changed, 104 insertions(+), 14 deletions(-) diff --git a/client/src/containers/User/Profile/ProfileUpdate.tsx b/client/src/containers/User/Profile/ProfileUpdate.tsx index c81d8ba..803ca29 100644 --- a/client/src/containers/User/Profile/ProfileUpdate.tsx +++ b/client/src/containers/User/Profile/ProfileUpdate.tsx @@ -1381,7 +1381,6 @@ const ProfileUpdate: React.FC = () => { const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); - // Load user profile useEffect(() => { const fetchUserData = async () => { setLoading(true); @@ -1419,7 +1418,6 @@ const ProfileUpdate: React.FC = () => { if (userId) fetchUserData(); }, [userId]); - // Handle file input change and convert to preview const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] ?? null; if (file) { @@ -1430,15 +1428,13 @@ const ProfileUpdate: React.FC = () => { } }; - // Convert image file to base64 - const fileToBase64 = (file: File): Promise => { - return new Promise((resolve, reject) => { + const fileToBase64 = (file: File): Promise => + new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result as string); reader.onerror = (err) => reject(err); }); - }; const validateForm = () => { const errors: Record = {}; @@ -1466,7 +1462,6 @@ const ProfileUpdate: React.FC = () => { return Object.keys(errors).length === 0; }; - // Submit handler const handleUserUpdate = async (e: React.FormEvent) => { e.preventDefault(); setError(null); @@ -1483,10 +1478,8 @@ const ProfileUpdate: React.FC = () => { const payload: any = { ...formData }; - // Remove password field if no new password if (!formData.newPassword) delete payload.password; - // Convert selected file to base64 if (selectedFile) { payload.profileImage = await fileToBase64(selectedFile); } @@ -1548,7 +1541,7 @@ const ProfileUpdate: React.FC = () => {
- {/* Form Fields */} + {/* ALL FORM FIELDS */} Email @@ -1573,10 +1566,107 @@ const ProfileUpdate: React.FC = () => { - {/* Add other fields similarly... */} - + + + User Type + setFormData({ ...formData, userType: e.target.value })} + /> + {formErrors.userType} + + + + Experience Level + setFormData({ ...formData, experienceLevel: e.target.value })} + /> + {formErrors.experienceLevel} + + + + + + Location + setFormData({ ...formData, location: e.target.value })} + /> + {formErrors.location} + + + + Available From + setFormData({ ...formData, availableFrom: e.target.value })} + /> + {formErrors.availableFrom} + + + + + + Phone Number + setFormData({ ...formData, phoneNumber: e.target.value })} + /> + {formErrors.phoneNumber} + + + + Driver’s License + setFormData({ ...formData, driversLicense: e.target.value })} + /> + {formErrors.driversLicense} + + + + + Comments + setFormData({ ...formData, comments: e.target.value })} + /> + {formErrors.comments} + + + + New Password + setFormData({ ...formData, newPassword: e.target.value })} + /> + + +
+ + + + +
);