// components/ProfilePage.jsx /** * Profile Page Component * * Displays the logged-in user's profile information and allows editing. * Handles profile updates via API calls and user sign-out. */ // React, Hooks, and PropTypes are globally available const ProfilePage = ({ user, onSignout }) => { // --- State --- const [isEditing, setIsEditing] = useState(false); // Initialize form data safely, handling cases where user might be null initially const [formData, setFormData] = useState({ full_name: user?.full_name || "", email: user?.email || "", profile_picture: user?.profile_picture || "", }); const [errors, setErrors] = useState({}); const [loading, setLoading] = useState(false); // For API call status const [message, setMessage] = useState(""); // Feedback message const [touched, setTouched] = useState({}); // API Endpoint const UPDATE_PROFILE_ENDPOINT = "/api/auth/update_profile.php"; // --- Effects --- // Update form data if the user prop changes (e.g., after initial load or external update) useEffect(() => { if (user) { // Only update form if not currently editing to avoid overwriting user input if (!isEditing) { setFormData({ full_name: user.full_name || "", email: user.email || "", profile_picture: user.profile_picture || "", }); } } else { // Handle case where user becomes null (e.g., logged out elsewhere) // Optionally redirect or show message window.location.hash = '#/signin'; } }, [user, isEditing]); // Re-run if user object changes OR if editing mode changes // --- Input Handling --- const handleChange = (e) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); // Clear validation error for the field being changed if it was touched if (touched[name] && errors[name]) { setErrors((prev) => ({ ...prev, [name]: "" })); } }; const handleBlur = (e) => { const { name, value } = e.target; setTouched((prev) => ({ ...prev, [name]: true })); validateField(name, value); // Validate the field when it loses focus }; // --- Validation --- const validateField = (name, value) => { let error = ""; switch (name) { case "full_name": if (!value.trim()) error = "Full name is required"; else if (value.length > 100) error = "Full name cannot exceed 100 characters."; break; case "email": if (!value.trim()) error = "Email is required"; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) error = "Invalid email format"; else if (value.length > 100) error = "Email cannot exceed 100 characters."; break; case "profile_picture": // Basic URL validation (optional, adjust as needed) // Allow empty string or null if (value && value.trim() && !/^https?:\/\/.+\..+/.test(value)) { error = "Please enter a valid URL (e.g., https://...)."; } else if (value && value.length > 255) { error = "Profile picture URL is too long (max 255 chars)."; } break; default: break; } // Update the errors state for this specific field setErrors((prev) => ({ ...prev, [name]: error })); return error === ""; // Return true if valid (no error) }; const validateForm = () => { let isValid = true; const fieldsToValidate = ["full_name", "email", "profile_picture"]; // Validate all fields and mark them as touched const allTouched = {}; fieldsToValidate.forEach((field) => { allTouched[field] = true; // Mark as touched if (!validateField(field, formData[field])) { // Validate and check result isValid = false; } }); setTouched(allTouched); // Update touched state return isValid; }; // --- Form Submission --- const handleSubmit = async (e) => { e.preventDefault(); setMessage(""); // Clear previous messages if (!validateForm()) { setMessage("Please fix the errors in the form."); return; } setLoading(true); const token = localStorage.getItem("token"); if (!token) { setMessage("Authentication error. Please sign in again."); setLoading(false); // Optionally trigger signout here if token is missing // onSignout(); return; } // Prepare data, ensuring empty profile picture URL becomes null for backend const dataToSend = { ...formData, profile_picture: formData.profile_picture?.trim() || null }; try { const response = await fetch(UPDATE_PROFILE_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(dataToSend), }); const data = await response.json(); if (!response.ok || !data.success) { // Handle specific backend validation errors if provided if (data.details) { setErrors((prev) => ({ ...prev, ...data.details })); } throw new Error(data.error || "Failed to update profile."); } // --- Update Successful --- setMessage("Profile updated successfully!"); setIsEditing(false); // Exit editing mode // NOTE: The user prop in App.jsx should ideally be updated. // The most robust way is to trigger a re-verification or pass an update handler. // For now, we update the local form state based on the successful API response // which might contain the fully updated user object. if (data.user) { setFormData({ // Update local form state from response full_name: data.user.full_name || "", email: data.user.email || "", profile_picture: data.user.profile_picture || "", }); // Optionally, call a function passed from App to update the global user state // if (typeof onProfileUpdate === 'function') { onProfileUpdate(data.user); } } setErrors({}); // Clear errors setTouched({}); // Reset touched state } catch (error) { console.error("Profile update error:", error); setMessage(error.message); setErrors(prev => ({ ...prev, form: error.message })); // Show general form error } finally { setLoading(false); } }; // --- Render Logic --- // Should not happen if App.jsx protects route, but good fallback if (!user) { // This case is now handled by the redirect in App.jsx's renderPage // but kept as a defensive measure. return (

Access Denied

Please sign in to view your profile.

Go to Sign In
); } // Display user info (non-editing mode) const renderDisplayMode = () => (
Username: {/* Username is typically not editable */} {user.username}
Full Name: {user.full_name || "-"}
Email: {user.email || "-"}
{/* Display profile picture URL if available */} {user.profile_picture && (
Picture URL: {user.profile_picture}
)}
{/* Use the onSignout prop passed from App.jsx */}
); // Display edit form (editing mode) const renderEditMode = () => (
{errors.full_name && touched.full_name &&

{errors.full_name}

}
{errors.email && touched.email &&

{errors.email}

}
{errors.profile_picture && touched.profile_picture &&

{errors.profile_picture}

}
); // --- Main Render --- return (
{/* Show current picture (or edited one optimistically if URL is valid) */} {(isEditing ? formData.profile_picture : user.profile_picture) ? ( Profile { e.target.style.display='none'; e.target.nextSibling.style.display='flex'; }} // Hide img, show fallback /> ) : null} {/* Fallback Icon - always present but hidden by img if src is valid */}

{isEditing ? "Edit Profile" : "My Profile"}

{/* Display feedback message */} {message && (
{message}
)} {/* Render either display or edit mode */} {isEditing ? renderEditMode() : renderDisplayMode()}
); }; // --- Prop Type Validation --- ProfilePage.propTypes = { user: PropTypes.shape({ id: PropTypes.number, username: PropTypes.string, email: PropTypes.string, full_name: PropTypes.string, profile_picture: PropTypes.string, }).isRequired, // User object is required for this page onSignout: PropTypes.func.isRequired, // Signout handler from App is required }; // Make ProfilePage globally available window.ProfilePage = ProfilePage;