diff --git a/client/src/App.jsx b/client/src/App.jsx index 97026e3..5878520 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -25,6 +25,7 @@ import AiSearch from "./pages/AiSearch.jsx"; import MeetingRoom from "./pages/MeetingRoom.jsx"; import MeetingDetails from "./pages/MeetingDetails.jsx"; import TeamMembers from "./pages/TeamMembers.jsx"; +import Profile from "./pages/Profile.jsx"; // --- Components --- import ProtectedRoute from "./components/ProtectedRoute.jsx"; @@ -230,6 +231,15 @@ const App = () => { } /> + + + + } + /> + {/* ✅ Fallback route — send unknown routes to Home */} } /> diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx index 44f6bec..2ecb1a3 100644 --- a/client/src/components/Navbar.jsx +++ b/client/src/components/Navbar.jsx @@ -36,6 +36,11 @@ const Navbar = () => { const [notificationsOpen, setNotificationsOpen] = useState(false); const [mobileNotifOpen, setMobileNotifOpen] = useState(false); const [scrolled, setScrolled] = useState(false); + const [imgFailed, setImgFailed] = useState(false); + + useEffect(() => { + setImgFailed(false); + }, [userData?.profilePic]); // Unread notifications mock state const [unreadCount, setUnreadCount] = useState(3); @@ -395,10 +400,20 @@ const Navbar = () => { aria-haspopup="true" aria-label="Open user menu" > -
- {userData?.name - ? userData.name.charAt(0).toUpperCase() - : "U"} +
+
+ {userData?.name + ? userData.name.charAt(0).toUpperCase() + : "U"} +
+ {userData?.profilePic && !imgFailed && ( + {userData.name} setImgFailed(true)} + /> + )}
{ /* Logged In Mobile Nav List */ <>
-
- {userData?.name ? userData.name.charAt(0).toUpperCase() : "U"} +
+
+ {userData?.name + ? userData.name.charAt(0).toUpperCase() + : "U"} +
+ {userData?.profilePic && !imgFailed && ( + {userData.name} setImgFailed(true)} + /> + )}

diff --git a/client/src/pages/Profile.jsx b/client/src/pages/Profile.jsx new file mode 100644 index 0000000..277e6b1 --- /dev/null +++ b/client/src/pages/Profile.jsx @@ -0,0 +1,451 @@ +import React, { useState, useContext, useEffect } from "react"; +import Navbar from "../components/Navbar.jsx"; +import axios from "axios"; +import { toast } from "react-toastify"; +import AppContent from "../context/AppContent"; +import { + User, + Mail, + Building2, + ShieldAlert, + Calendar, + Edit2, + X, + Check, + Loader2, + ShieldCheck, + Globe, +} from "lucide-react"; + +const Profile = () => { + const { backendUrl, userData, setUserData } = useContext(AppContent); + const [isEditing, setIsEditing] = useState(false); + const [loading, setLoading] = useState(false); + const [profilePicFailed, setProfilePicFailed] = useState(false); + + useEffect(() => { + setProfilePicFailed(false); + }, [userData?.profilePic]); + + // Form State + const [name, setName] = useState(""); + const [profilePic, setProfilePic] = useState(""); + const [bio, setBio] = useState(""); + + // Validation States + const [errors, setErrors] = useState({ + name: "", + profilePic: "", + }); + + // Load user data into form + useEffect(() => { + if (userData) { + setName(userData.name || ""); + setProfilePic(userData.profilePic || ""); + setBio(userData.bio || ""); + } + }, [userData]); + + if (!userData) { + return ( +

+ +
+ + + Loading profile... + +
+
+ ); + } + + // Get Initials Helper + const getInitials = (userName) => { + if (!userName) return "U"; + const parts = userName.trim().split(" "); + if (parts.length === 1) return parts[0].charAt(0).toUpperCase(); + return ( + parts[0].charAt(0) + parts[parts.length - 1].charAt(0) + ).toUpperCase(); + }; + + // Form Validations + const validateForm = () => { + const newErrors = { name: "", profilePic: "" }; + let isValid = true; + + if (!name.trim()) { + newErrors.name = "Full Name is required."; + isValid = false; + } else if (name.trim().length < 2) { + newErrors.name = "Full Name must be at least 2 characters."; + isValid = false; + } + + if (profilePic.trim()) { + try { + const u = new URL(profilePic.trim()); + if (u.protocol !== "http:" && u.protocol !== "https:") { + newErrors.profilePic = "Image URL must use http or https."; + isValid = false; + } + } catch { + newErrors.profilePic = + "Please enter a valid URL (starting with http:// or https://)."; + isValid = false; + } + } + + setErrors(newErrors); + return isValid; + }; + + // Save changes handler + const handleSave = async (e) => { + e.preventDefault(); + if (!validateForm()) return; + + setLoading(true); + try { + const { data } = await axios.put( + `${backendUrl}/api/user/update`, + { + name: name.trim(), + profilePic: profilePic.trim(), + bio: bio.trim(), + }, + { withCredentials: true }, + ); + + if (data.success) { + toast.success(data.message || "Profile updated successfully!"); + setUserData(data.user); + localStorage.setItem("userData", JSON.stringify(data.user)); + setIsEditing(false); + } else { + toast.error(data.message || "Failed to update profile."); + } + } catch (err) { + console.error("Profile update error:", err); + const msg = + err.response?.data?.message || "Server error while updating profile."; + toast.error(msg); + } finally { + setLoading(false); + } + }; + + // Discard changes / Reset form + const handleCancel = () => { + setName(userData.name || ""); + setProfilePic(userData.profilePic || ""); + setBio(userData.bio || ""); + setErrors({ name: "", profilePic: "" }); + setIsEditing(false); + }; + + // Formatted date string (e.g. Mar 2025) + const formattedMemberSince = userData.createdAt + ? new Date(userData.createdAt).toLocaleDateString("en-US", { + month: "short", + year: "numeric", + }) + : "N/A"; + + const displayRole = userData.role + ? userData.role.charAt(0).toUpperCase() + + userData.role.slice(1).toLowerCase() + : "Member"; + + return ( +
+ + +
+ {/* Page title header */} +
+

+ User Profile +

+

+ Manage your personal credentials, view your organization link, and + customize your bio. +

+
+ + {/* Profile Card component - exact reference design in light theme */} +
+ {/* Toggled content */} + {!isEditing ? ( + // ================= VIEW STATE ================= +
+ {/* Header section */} +
+
+ {/* Custom initials / profile image */} + {userData.profilePic && !profilePicFailed ? ( + {userData.name} { + toast.warning( + "Failed to load custom profile image. Displaying initials fallback.", + ); + setProfilePic(""); + setProfilePicFailed(true); + const cleared = { ...userData, profilePic: "" }; + setUserData(cleared); + localStorage.setItem( + "userData", + JSON.stringify(cleared), + ); + try { + await axios.put( + `${backendUrl}/api/user/update`, + { + name: userData.name, + profilePic: "", + bio: userData.bio, + }, + { withCredentials: true }, + ); + } catch { + // silent + } + }} + /> + ) : ( +
+ {getInitials(userData.name)} +
+ )} + +
+

+ {userData.name} +

+
+ + {displayRole} + + {userData.isAccountVerified ? ( + + + Verified + + ) : ( + + + Unverified + + )} +
+
+
+ + +
+ + {/* Grid details section */} +
+
+
+ + Email +
+
+ {userData.email} +
+
+ +
+
+ + Organization +
+
+ {userData.organization?.name || "No Organization"} +
+
+ +
+
+ + Role +
+
+ {displayRole} +
+
+ +
+
+ + Member since +
+
+ {formattedMemberSince} +
+
+
+ + {/* Bio section */} +
+
+ Bio +
+

+ {userData.bio || "No bio added yet. Tell us about yourself!"} +

+
+
+ ) : ( + // ================= EDIT STATE ================= +
+
+

+ Edit Profile Details +

+ +
+ +
+ {/* Full Name input */} +
+ +
+ + setName(e.target.value)} + placeholder="Your Full Name" + disabled={loading} + className={`w-full bg-slate-50/50 hover:bg-slate-50 border ${ + errors.name + ? "border-red-500/80" + : "border-slate-200 focus:border-blue-500 focus:bg-white" + } rounded-xl py-2.5 pl-10 pr-4 text-sm text-slate-800 placeholder-slate-400 transition-all outline-none`} + /> +
+ {errors.name && ( +

+ {errors.name} +

+ )} +
+ + {/* Profile Picture URL input */} +
+ +
+ + setProfilePic(e.target.value)} + placeholder="https://example.com/avatar.jpg" + disabled={loading} + className={`w-full bg-slate-50/50 hover:bg-slate-50 border ${ + errors.profilePic + ? "border-red-500/80" + : "border-slate-200 focus:border-blue-500 focus:bg-white" + } rounded-xl py-2.5 pl-10 pr-4 text-sm text-slate-800 placeholder-slate-400 transition-all outline-none`} + /> +
+ {errors.profilePic && ( +

+ {errors.profilePic} +

+ )} +
+ + {/* Bio text input */} +
+ +