827 lines
31 KiB
JavaScript
827 lines
31 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';
|
|
|
|
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 [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 */}
|
|
<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)]">
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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 */}
|
|
<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;
|