Files
membership-fe/src/pages/Profile.js
Andika d4acef8d90 - Created useDirectoryConfig hook (src/hooks/use-directory-config.js)
- Updated Profile.js - conditional rendering with isFieldEnabled()
- Updated MemberCard.js - conditional rendering for directory fields
- Updated MembersDirectory.js - conditional rendering in profile dialog
- Created AdminDirectorySettings.js - Admin UI for toggling fields
- Updated SettingsSidebar.js - Added Directory and Registration tabs
- Updated App.js - Added routes for new settings pages
2026-02-02 17:08:11 +07:00

845 lines
32 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import { useAuth } from '../context/AuthContext';
import api from '../utils/api';
import { Card } from '../components/ui/card';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Label } from '../components/ui/label';
import { Textarea } from '../components/ui/textarea';
import { toast } from 'sonner';
import Navbar from '../components/Navbar';
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 PaymentMethodsSection from '../components/PaymentMethodsSection';
import { useNavigate } from 'react-router-dom';
import useDirectoryConfig from '../hooks/use-directory-config';
const Profile = () => {
const { user } = useAuth();
const [loading, setLoading] = useState(false);
const [profileData, setProfileData] = useState(null);
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
const [profilePhotoUrl, setProfilePhotoUrl] = useState(null);
const [previewImage, setPreviewImage] = useState(null);
const [uploadingPhoto, setUploadingPhoto] = useState(false);
const fileInputRef = useRef(null);
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 { isFieldEnabled, loading: directoryConfigLoading } = useDirectoryConfig();
const [formData, setFormData] = useState({
first_name: '',
last_name: '',
phone: '',
address: '',
city: '',
state: '',
zipcode: '',
partner_first_name: '',
partner_last_name: '',
partner_is_member: false,
partner_plan_to_become_member: false,
newsletter_publish_name: false,
newsletter_publish_photo: false,
newsletter_publish_birthday: false,
newsletter_publish_none: false,
volunteer_interests: [],
show_in_directory: false,
directory_email: '',
directory_bio: '',
directory_address: '',
directory_phone: '',
directory_dob: '',
directory_partner_name: ''
});
useEffect(() => {
fetchConfig();
fetchProfile();
}, []);
// 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');
setMaxFileSizeMB(response.data.max_file_size_mb);
setMaxFileSizeBytes(response.data.max_file_size_bytes);
} catch (error) {
console.error('Failed to fetch config, using defaults:', error);
}
};
const fetchProfile = async () => {
try {
const response = await api.get('/users/profile');
setProfileData(response.data);
setProfilePhotoUrl(response.data.profile_photo_url);
setPreviewImage(response.data.profile_photo_url);
const newFormData = {
first_name: response.data.first_name || '',
last_name: response.data.last_name || '',
phone: response.data.phone || '',
address: response.data.address || '',
city: response.data.city || '',
state: response.data.state || '',
zipcode: response.data.zipcode || '',
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_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: response.data.volunteer_interests || [],
show_in_directory: response.data.show_in_directory || false,
directory_email: response.data.directory_email || '',
directory_bio: response.data.directory_bio || '',
directory_address: response.data.directory_address || '',
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 handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleCheckboxChange = (e) => {
const { name, checked } = e.target;
setFormData(prev => ({ ...prev, [name]: checked }));
};
const handleVolunteerToggle = (interest) => {
setFormData(prev => ({
...prev,
volunteer_interests: prev.volunteer_interests.includes(interest)
? prev.volunteer_interests.filter(i => i !== interest)
: [...prev.volunteer_interests, interest]
}));
};
const volunteerOptions = [
'Event Planning',
'Social Media',
'Newsletter',
'Fundraising',
'Community Outreach',
'Graphic Design',
'Photography',
'Writing/Editing',
'Tech Support',
'Hospitality'
];
const handlePhotoUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
toast.error('Please select an image file');
return;
}
if (file.size > maxFileSizeBytes) {
toast.error(`File size must be less than ${maxFileSizeMB}MB`);
return;
}
setUploadingPhoto(true);
try {
const formData = new FormData();
formData.append('file', file);
const response = await api.post('/members/profile/upload-photo', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
setProfilePhotoUrl(response.data.profile_photo_url);
setPreviewImage(response.data.profile_photo_url);
toast.success('Profile photo updated successfully!');
} catch (error) {
toast.error('Failed to upload photo');
} finally {
setUploadingPhoto(false);
}
};
const handlePhotoDelete = async () => {
if (!profilePhotoUrl) return;
setUploadingPhoto(true);
try {
await api.delete('/members/profile/delete-photo');
setProfilePhotoUrl(null);
setPreviewImage(null);
toast.success('Profile photo deleted successfully!');
} catch (error) {
toast.error('Failed to delete photo');
} finally {
setUploadingPhoto(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
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');
} finally {
setLoading(false);
}
};
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 (
<div className="min-h-screen bg-white dark:bg-[var(--purple-deep)]">
<Navbar />
<div className="flex items-center justify-center min-h-[60vh]">
<p className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p>
</div>
</div>
);
}
// Account & Privacy Tab Content
const AccountPrivacyContent = () => (
<div className="space-y-6 ">
<Card className="space-y-6 px-6 pb-6">
<div className="bg-brand-purple text-white px-4 py-3 rounded-t-xl -mx-6 -mt-6 mb-6">
<h3 className="font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>Account & Privacy</h3>
</div>
<div>
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Login Email</p>
<p className="text-[var(--purple-ink)] dark:text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.email}</p>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Password</p>
<p className="text-[var(--purple-ink)] dark:text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}></p>
</div>
<Button
type="button"
onClick={() => setPasswordDialogOpen(true)}
variant="outline"
className="border-2 border-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-[var(--lavender-300)] rounded-lg px-4 py-2"
>
Change
</Button>
</div>
<div>
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Membership Status</p>
<p className="text-[var(--purple-ink)] dark:text-[var(--purple-ink)] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{profileData.status?.replace('_', ' ') || 'Active'}
</p>
</div>
</Card>
{/* Payment Methods Section */}
<PaymentMethodsSection />
</div>
);
// My Bio & Directory Tab Content
const BioDirectoryContent = () => (
<Card className="space-y-6 px-6 pb-6">
<div className="bg-brand-purple text-white px-4 py-3 rounded-t-lg -mx-6 -mt-6 mb-6">
<h3 className="font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>My Bio & Directory</h3>
</div>
{/* Profile Photo Section */}
<div className="pb-6 border-b border-[var(--neutral-800)]">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] mb-4 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Camera className="h-5 w-5 text-brand-purple" />
Profile Photo
</h4>
<div className="flex flex-col md:flex-row items-center gap-6">
<Avatar className="h-24 w-24 border-4 border-[var(--neutral-800)]">
<AvatarImage src={previewImage} alt="Profile" />
<AvatarFallback className="bg-[var(--lavender-300)] text-brand-purple text-2xl">
{profileData?.first_name?.charAt(0)}{profileData?.last_name?.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-3">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handlePhotoUpload}
className="hidden"
/>
<Button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={uploadingPhoto}
className="bg-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full px-4 py-2"
>
<Upload className="h-4 w-4 mr-2" />
{uploadingPhoto ? 'Uploading...' : 'Upload Photo'}
</Button>
{profilePhotoUrl && (
<Button
type="button"
onClick={handlePhotoDelete}
disabled={uploadingPhoto}
variant="outline"
className="border-2 border-red-500 text-red-500 hover:bg-red-50 rounded-full px-4 py-2"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Photo
</Button>
)}
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Max {maxFileSizeMB}MB
</p>
</div>
</div>
</div>
{/* Personal Information */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Personal Information
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label htmlFor="first_name">First Name</Label>
<Input
id="first_name"
name="first_name"
value={formData.first_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="first-name-input"
/>
</div>
<div>
<Label htmlFor="last_name">Last Name</Label>
<Input
id="last_name"
name="last_name"
value={formData.last_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="last-name-input"
/>
</div>
</div>
<div>
<Label htmlFor="phone">Phone</Label>
<Input
id="phone"
name="phone"
type="tel"
value={formData.phone}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="phone-input"
/>
</div>
<div>
<Label htmlFor="address">Address</Label>
<Input
id="address"
name="address"
value={formData.address}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="address-input"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<Label htmlFor="city">City</Label>
<Input
id="city"
name="city"
value={formData.city}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="city-input"
/>
</div>
<div>
<Label htmlFor="state">State</Label>
<Input
id="state"
name="state"
value={formData.state}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="state-input"
/>
</div>
<div>
<Label htmlFor="zipcode">Zipcode</Label>
<Input
id="zipcode"
name="zipcode"
value={formData.zipcode}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="zipcode-input"
/>
</div>
</div>
</div>
{/* Member Directory Settings */}
{isFieldEnabled('show_in_directory') && (
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<BookUser className="h-5 w-5 text-[var(--orange-light)]" />
Member Directory Settings
</h4>
<p className="text-brand-purple text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Control your visibility and information in the member directory.
</p>
<div className="flex items-center gap-3 p-4 bg-[var(--lavender-400)] rounded-lg">
<input
type="checkbox"
id="show_in_directory"
name="show_in_directory"
checked={formData.show_in_directory}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="show_in_directory" className="cursor-pointer text-[var(--purple-ink)] font-medium">
Include me in the member directory
</Label>
</div>
{formData.show_in_directory && (
<div className="space-y-4 pl-4 border-l-4 border-[var(--neutral-800)]">
{isFieldEnabled('directory_email') && (
<div>
<Label htmlFor="directory_email">Directory Email</Label>
<Input
id="directory_email"
name="directory_email"
type="email"
value={formData.directory_email}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional - email to show in directory"
/>
</div>
)}
{isFieldEnabled('directory_bio') && (
<div>
<Label htmlFor="directory_bio">Bio</Label>
<Textarea
id="directory_bio"
name="directory_bio"
value={formData.directory_bio}
onChange={handleInputChange}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple min-h-[100px]"
placeholder="Tell other members about yourself..."
/>
</div>
)}
{isFieldEnabled('directory_address') && (
<div>
<Label htmlFor="directory_address">Address</Label>
<Input
id="directory_address"
name="directory_address"
value={formData.directory_address}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional - address to show in directory"
/>
</div>
)}
{isFieldEnabled('directory_phone') && (
<div>
<Label htmlFor="directory_phone">Phone</Label>
<Input
id="directory_phone"
name="directory_phone"
type="tel"
value={formData.directory_phone}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional - phone to show in directory"
/>
</div>
)}
{isFieldEnabled('directory_dob') && (
<div>
<Label htmlFor="directory_dob">Date of Birth</Label>
<Input
id="directory_dob"
name="directory_dob"
type="date"
value={formData.directory_dob}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
</div>
)}
{isFieldEnabled('directory_partner_name') && (
<div>
<Label htmlFor="directory_partner_name">Partner Name</Label>
<Input
id="directory_partner_name"
name="directory_partner_name"
value={formData.directory_partner_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional - partner name to show in directory"
/>
</div>
)}
</div>
)}
</div>
)}
</Card>
);
// Engagement Tab Content
const EngagementContent = () => (
<Card className="space-y-6 px-6 pb-6">
<div className="bg-brand-purple text-white px-4 py-3 rounded-t-lg -mx-6 -mt-6 mb-6">
<h3 className="font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>Engagement</h3>
</div>
{/* Partner Information */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
Partner Information
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label htmlFor="partner_first_name">Partner First Name</Label>
<Input
id="partner_first_name"
name="partner_first_name"
value={formData.partner_first_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional"
/>
</div>
<div>
<Label htmlFor="partner_last_name">Partner Last Name</Label>
<Input
id="partner_last_name"
name="partner_last_name"
value={formData.partner_last_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="partner_is_member"
name="partner_is_member"
checked={formData.partner_is_member}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="partner_is_member" className="cursor-pointer text-[var(--purple-ink)]">
My partner is a current member
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="partner_plan_to_become_member"
name="partner_plan_to_become_member"
checked={formData.partner_plan_to_become_member}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="partner_plan_to_become_member" className="cursor-pointer text-[var(--purple-ink)]">
My partner plans to become a member
</Label>
</div>
</div>
</div>
{/* Newsletter Preferences */}
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Mail className="h-5 w-5 text-[var(--green-light)]" />
Newsletter Preferences
</h4>
<p className="text-brand-purple text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Choose what information you'd like published in our member newsletter.
</p>
<div className="space-y-3">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_name"
name="newsletter_publish_name"
checked={formData.newsletter_publish_name}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="newsletter_publish_name" className="cursor-pointer text-[var(--purple-ink)]">
Publish my name
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_photo"
name="newsletter_publish_photo"
checked={formData.newsletter_publish_photo}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="newsletter_publish_photo" className="cursor-pointer text-[var(--purple-ink)]">
Publish my photo
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_birthday"
name="newsletter_publish_birthday"
checked={formData.newsletter_publish_birthday}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="newsletter_publish_birthday" className="cursor-pointer text-[var(--purple-ink)]">
Publish my birthday
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_none"
name="newsletter_publish_none"
checked={formData.newsletter_publish_none}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="newsletter_publish_none" className="cursor-pointer text-[var(--purple-ink)]">
Do not publish any information
</Label>
</div>
</div>
</div>
{/* Volunteer Interests */}
{isFieldEnabled('volunteer_interests') && (
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Users className="h-5 w-5 text-brand-purple" />
Volunteer Interests
</h4>
<p className="text-brand-purple text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Select areas where you'd like to volunteer and help our community.
</p>
<div className="grid md:grid-cols-2 gap-3">
{volunteerOptions.map(option => (
<div key={option} className="flex items-center gap-3">
<input
type="checkbox"
id={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
checked={formData.volunteer_interests.includes(option)}
onChange={() => handleVolunteerToggle(option)}
className="ui-checkbox"
/>
<Label
htmlFor={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
className="cursor-pointer text-[var(--purple-ink)]"
>
{option}
</Label>
</div>
))}
</div>
</div>
)}
</Card>
);
return (
<div className="min-h-screen bg-background flex flex-col">
<Navbar />
<div className="flex-1 flex flex-col">
<div className="max-w-5xl mx-auto px-4 sm:px-6 py-8 w-full flex-1 pb-24">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className='space-y-4'>
<h1 className="text-4xl md:text-4xl font-semibold " style={{ fontFamily: "'Inter', sans-serif" }}>
My Profile
</h1>
<p className='text-brand-purple text-md'>Update your personal information below.</p>
</div>
{/* <Button
type="button"
variant="outline"
className="border-2 hover:bg-white/10 rounded-lg px-4 py-2"
>
<Eye className="h-4 w-4 mr-2 md:mr-2" />
<span className="hidden md:inline">Public Profile Preview</span>
<span className="md:hidden">Preview</span>
</Button> */}
</div>
{/* Main Content Div */}
<div className="overflow-hidden ">
<form onSubmit={handleSubmit} data-testid="profile-form">
{/* Mobile Tabs */}
<div className="md:hidden flex border-b border-[var(--neutral-800)] mb-4 gap-1 ">
{tabs.map((tab) => {
const IconComponent = tab.icon;
return (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={`flex-1 flex flex-col items-center rounded-xl gap-1 px-3 py-3 text-xs font-medium transition-colors ${activeTab === tab.id
? 'bg-brand-purple text-white'
: 'text-[var(--purple-ink)] hover:bg-[var(--lavender-300)]'
}`}
>
<IconComponent className="h-5 w-5" />
<span className="whitespace-nowrap">{tab.shortLabel}</span>
</button>
);
})}
</div>
{/* Desktop Layout */}
<div className="flex">
{/* Desktop Sidebar Tabs */}
<div className="hidden md:flex flex-col w-64 border-[var(--neutral-800)] mr-4 gap-2">
{tabs.map((tab) => {
const IconComponent = tab.icon;
return (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-left font-medium transition-colors ${activeTab === tab.id
? 'bg-brand-purple text-white'
: 'text-[var(--purple-ink)] hover:bg-[var(--lavender-300)]'
}`}
>
<IconComponent className="h-5 w-5" />
<span>{tab.label}</span>
</button>
);
})}
</div>
{/* Content Area */}
<div className="flex-1 p-6 min-h-[500px]">
{activeTab === 'account' && <AccountPrivacyContent />}
{activeTab === 'bio' && <BioDirectoryContent />}
{activeTab === 'engagement' && <EngagementContent />}
</div>
</div>
</form>
</div>
</div>
{/* Sticky Footer */}
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-[var(--neutral-800)] px-4 sm:px-6 py-4 z-50">
<div className="max-w-5xl px-6 mx-auto flex items-center justify-between">
<div className='flex gap-2 w-full lg:justify-between md:mr-5'>
<Button
onClick={() => navigate(-1)}
className="h-fit bg-brand-purple hover:bg-brand-purple/80 rounded-lg px-6 py-2 font-medium shadow-lg w-full md:w-auto">
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
<div className="flex items-center gap-2">
{hasUnsavedChanges && (
<>
<span className="h-3 w-3 rounded-full bg-[var(--orange-light)]"></span>
<span className="text-sm text-[var(--purple-ink)] " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Unsaved changes
</span>
</>
)}
</div>
<Button
type="button"
onClick={handleSubmit}
disabled={loading}
className="bg-brand-purple text-white hover:bg-brand-dark-lavender rounded-lg px-6 py-2 font-medium shadow-lg disabled:opacity-50 w-full md:w-auto"
data-testid="save-profile-button"
>
{loading ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
</div>
</div>
<ChangePasswordDialog
open={passwordDialogOpen}
onOpenChange={setPasswordDialogOpen}
/>
</div>
);
};
export default Profile;