diff --git a/src/pages/Profile.js b/src/pages/Profile.js index 7852fab..a56965f 100644 --- a/src/pages/Profile.js +++ b/src/pages/Profile.js @@ -1,6 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; import { useAuth } from '../context/AuthContext'; -import { useNavigate } from 'react-router-dom'; import api from '../utils/api'; import { Card } from '../components/ui/card'; import { Button } from '../components/ui/button'; @@ -9,11 +8,10 @@ import { Label } from '../components/ui/label'; import { Textarea } from '../components/ui/textarea'; import { toast } from 'sonner'; import Navbar from '../components/Navbar'; -import MemberFooter from '../components/MemberFooter'; -import { User, ArrowLeft, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react'; +import { User, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2, Eye, CreditCard, Handshake, ArrowLeft } from 'lucide-react'; import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar'; import ChangePasswordDialog from '../components/ChangePasswordDialog'; -import TransactionHistory from '../components/TransactionHistory'; +import { useNavigate } from 'react-router-dom'; const Profile = () => { const { user } = useAuth(); @@ -24,13 +22,13 @@ const Profile = () => { const [previewImage, setPreviewImage] = useState(null); const [uploadingPhoto, setUploadingPhoto] = useState(false); const fileInputRef = useRef(null); - const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); // Default 50MB - const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes - const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] }); - const [transactionsLoading, setTransactionsLoading] = useState(true); + const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); + const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); + const [activeTab, setActiveTab] = useState('account'); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [initialFormData, setInitialFormData] = useState(null); const navigate = useNavigate(); const [formData, setFormData] = useState({ - // Personal Information first_name: '', last_name: '', phone: '', @@ -38,19 +36,15 @@ const Profile = () => { city: '', state: '', zipcode: '', - // Partner Information partner_first_name: '', partner_last_name: '', partner_is_member: false, partner_plan_to_become_member: false, - // Newsletter Preferences newsletter_publish_name: false, newsletter_publish_photo: false, newsletter_publish_birthday: false, newsletter_publish_none: false, - // Volunteer Interests (array) volunteer_interests: [], - // Member Directory Settings show_in_directory: false, directory_email: '', directory_bio: '', @@ -63,9 +57,16 @@ const Profile = () => { useEffect(() => { fetchConfig(); fetchProfile(); - fetchTransactions(); }, []); + // Track unsaved changes + useEffect(() => { + if (initialFormData) { + const hasChanges = JSON.stringify(formData) !== JSON.stringify(initialFormData); + setHasUnsavedChanges(hasChanges); + } + }, [formData, initialFormData]); + const fetchConfig = async () => { try { const response = await api.get('/config'); @@ -73,7 +74,6 @@ const Profile = () => { setMaxFileSizeBytes(response.data.max_file_size_bytes); } catch (error) { console.error('Failed to fetch config, using defaults:', error); - // Keep default values if fetch fails } }; @@ -83,8 +83,7 @@ const Profile = () => { setProfileData(response.data); setProfilePhotoUrl(response.data.profile_photo_url); setPreviewImage(response.data.profile_photo_url); - setFormData({ - // Personal Information + const newFormData = { first_name: response.data.first_name || '', last_name: response.data.last_name || '', phone: response.data.phone || '', @@ -92,19 +91,15 @@ const Profile = () => { city: response.data.city || '', state: response.data.state || '', zipcode: response.data.zipcode || '', - // Partner Information partner_first_name: response.data.partner_first_name || '', partner_last_name: response.data.partner_last_name || '', partner_is_member: response.data.partner_is_member || false, partner_plan_to_become_member: response.data.partner_plan_to_become_member || false, - // Newsletter Preferences newsletter_publish_name: response.data.newsletter_publish_name || false, newsletter_publish_photo: response.data.newsletter_publish_photo || false, newsletter_publish_birthday: response.data.newsletter_publish_birthday || false, newsletter_publish_none: response.data.newsletter_publish_none || false, - // Volunteer Interests volunteer_interests: response.data.volunteer_interests || [], - // Member Directory Settings show_in_directory: response.data.show_in_directory || false, directory_email: response.data.directory_email || '', directory_bio: response.data.directory_bio || '', @@ -112,25 +107,14 @@ const Profile = () => { directory_phone: response.data.directory_phone || '', directory_dob: response.data.directory_dob || '', directory_partner_name: response.data.directory_partner_name || '' - }); + }; + setFormData(newFormData); + setInitialFormData(newFormData); } catch (error) { toast.error('Failed to load profile'); } }; - const fetchTransactions = async () => { - try { - setTransactionsLoading(true); - const response = await api.get('/members/transactions'); - setTransactions(response.data); - } catch (error) { - console.error('Failed to load transactions:', error); - // Don't show error toast - transactions are optional - } finally { - setTransactionsLoading(false); - } - }; - const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); @@ -150,7 +134,6 @@ const Profile = () => { })); }; - // Volunteer interest options const volunteerOptions = [ 'Event Planning', 'Social Media', @@ -168,13 +151,11 @@ const Profile = () => { const file = e.target.files[0]; if (!file) return; - // Validate file type if (!file.type.startsWith('image/')) { toast.error('Please select an image file'); return; } - // Validate file size if (file.size > maxFileSizeBytes) { toast.error(`File size must be less than ${maxFileSizeMB}MB`); return; @@ -222,6 +203,8 @@ const Profile = () => { try { await api.put('/users/profile', formData); toast.success('Profile updated successfully!'); + setInitialFormData(formData); + setHasUnsavedChanges(false); fetchProfile(); } catch (error) { toast.error('Failed to update profile'); @@ -230,510 +213,623 @@ const Profile = () => { } }; + const tabs = [ + { id: 'account', label: 'Account & Privacy', shortLabel: 'Account', icon: Lock }, + { id: 'bio', label: 'My Bio & Directory', shortLabel: 'Bio & Directory', icon: User }, + { id: 'engagement', label: 'Engagement', shortLabel: 'Engagement', icon: Handshake } + ]; + if (!profileData) { return ( -
+
-

Loading profile...

+

Loading profile...

); } - return ( -
- + // Account & Privacy Tab Content + const AccountPrivacyContent = () => ( +
-
-
-
-

- My Profile -

-

- Update your personal information below. -

+ +
+

Account & Privacy

+
+
+

Login Email

+

{profileData.email}

+
+ +
+
+

Password

+

••••••••

- {/* Todo: functional back button */}
- - {/* Read-only Information */} -
-

- - Account Information -

-
-
-

Email

-

{profileData.email}

-
-
-

Status

-

{profileData.status.replace('_', ' ')}

-
-
-

Role

-

{profileData.role}

-
-
-

Date of Birth

-

- {new Date(profileData.date_of_birth).toLocaleDateString()} -

-
-
+
+

Membership Status

+

+ {profileData.status?.replace('_', ' ') || 'Active'} +

+
-
+
+
+

Payment Method

+
+ +
+
+ +
+ +
+ ); + + // My Bio & Directory Tab Content + const BioDirectoryContent = () => ( + +
+

My Bio & Directory

+
+ + {/* Profile Photo Section */} +
+

+ + Profile Photo +

+
+ + + + {profileData?.first_name?.charAt(0)}{profileData?.last_name?.charAt(0)} + + + +
+ + + + + {profilePhotoUrl && ( -
+ )} + +

+ Max {maxFileSizeMB}MB +

+
+
- {/* Profile Photo Section */} -
-

- - Profile Photo -

-
- - - - {profileData?.first_name?.charAt(0)}{profileData?.last_name?.charAt(0)} - - + {/* Personal Information */} +
+

+ Personal Information +

-
- - - - - {profilePhotoUrl && ( - - )} - -

- Upload a profile photo (Max {maxFileSizeMB}MB) -

-
-
+
+
+ +
+
+ + +
+
- {/* Editable Form */} -
-

- Personal Information -

+
+ + +
-
-
- - -
-
- - -
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Member Directory Settings */} +
+

+ + Member Directory Settings +

+

+ Control your visibility and information in the member directory. +

+ +
+ + +
+ + {formData.show_in_directory && ( +
+
+ +
- + +