Donation Tracking\ Validation Rejection\ Subscription Data Export\ Admin Dashboard Logo\ Admin Navbar Reorganization
712 lines
29 KiB
JavaScript
712 lines
29 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 MemberFooter from '../components/MemberFooter';
|
|
import { User, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react';
|
|
import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar';
|
|
import ChangePasswordDialog from '../components/ChangePasswordDialog';
|
|
|
|
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); // Default 50MB
|
|
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes
|
|
const [formData, setFormData] = useState({
|
|
// Personal Information
|
|
first_name: '',
|
|
last_name: '',
|
|
phone: '',
|
|
address: '',
|
|
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: '',
|
|
directory_address: '',
|
|
directory_phone: '',
|
|
directory_dob: '',
|
|
directory_partner_name: ''
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchConfig();
|
|
fetchProfile();
|
|
}, []);
|
|
|
|
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);
|
|
// Keep default values if fetch fails
|
|
}
|
|
};
|
|
|
|
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);
|
|
setFormData({
|
|
// Personal Information
|
|
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 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 || '',
|
|
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 || ''
|
|
});
|
|
} 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]
|
|
}));
|
|
};
|
|
|
|
// Volunteer interest options
|
|
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;
|
|
|
|
// 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;
|
|
}
|
|
|
|
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!');
|
|
fetchProfile();
|
|
} catch (error) {
|
|
toast.error('Failed to update profile');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (!profileData) {
|
|
return (
|
|
<div className="min-h-screen bg-white">
|
|
<Navbar />
|
|
<div className="flex items-center justify-center min-h-[60vh]">
|
|
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white">
|
|
<Navbar />
|
|
|
|
<div className="max-w-4xl mx-auto px-6 py-12">
|
|
<div className="mb-8">
|
|
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
My Profile
|
|
</h1>
|
|
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Update your personal information below.
|
|
</p>
|
|
</div>
|
|
|
|
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg">
|
|
{/* Read-only Information */}
|
|
<div className="mb-8 pb-8 border-b border-[#ddd8eb]">
|
|
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
<User className="h-6 w-6 text-[#664fa3]" />
|
|
Account Information
|
|
</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
|
<div>
|
|
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
|
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.email}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Status</p>
|
|
<p className="text-[#422268] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.status.replace('_', ' ')}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Role</p>
|
|
<p className="text-[#422268] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.role}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Date of Birth</p>
|
|
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{new Date(profileData.date_of_birth).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
<Button
|
|
type="button"
|
|
onClick={() => setPasswordDialogOpen(true)}
|
|
variant="outline"
|
|
className="border-2 border-[#664fa3] text-[#664fa3] hover:bg-[#f1eef9] rounded-full px-6 py-3"
|
|
>
|
|
<Lock className="h-4 w-4 mr-2" />
|
|
Change Password
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Profile Photo Section */}
|
|
<div className="pb-8 mb-8 border-b border-[#ddd8eb]">
|
|
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
<Camera className="h-6 w-6 text-[#664fa3]" />
|
|
Profile Photo
|
|
</h2>
|
|
<div className="flex flex-col md:flex-row items-center gap-6">
|
|
<Avatar className="h-32 w-32 border-4 border-[#ddd8eb]">
|
|
<AvatarImage src={previewImage} alt="Profile" />
|
|
<AvatarFallback className="bg-[#f1eef9] text-[#664fa3] text-3xl">
|
|
{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-[#664fa3] text-white hover:bg-[#422268] rounded-full px-6 py-3"
|
|
>
|
|
<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-6 py-3"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Delete Photo
|
|
</Button>
|
|
)}
|
|
|
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Upload a profile photo (Max {maxFileSizeMB}MB)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Editable Form */}
|
|
<form onSubmit={handleSubmit} className="space-y-6" data-testid="profile-form">
|
|
<h2 className="text-2xl font-semibold text-[#422268] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Personal Information
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
|
<div>
|
|
<Label htmlFor="first_name">First Name</Label>
|
|
<Input
|
|
id="first_name"
|
|
name="first_name"
|
|
value={formData.first_name}
|
|
onChange={handleInputChange}
|
|
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
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-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
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-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
data-testid="phone-input"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="address">Address</Label>
|
|
<Input
|
|
id="address"
|
|
name="address"
|
|
value={formData.address}
|
|
onChange={handleInputChange}
|
|
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
data-testid="address-input"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 sm:gap-6">
|
|
<div>
|
|
<Label htmlFor="city">City</Label>
|
|
<Input
|
|
id="city"
|
|
name="city"
|
|
value={formData.city}
|
|
onChange={handleInputChange}
|
|
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
data-testid="city-input"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="state">State</Label>
|
|
<Input
|
|
id="state"
|
|
name="state"
|
|
value={formData.state}
|
|
onChange={handleInputChange}
|
|
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
data-testid="state-input"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="zipcode">Zipcode</Label>
|
|
<Input
|
|
id="zipcode"
|
|
name="zipcode"
|
|
value={formData.zipcode}
|
|
onChange={handleInputChange}
|
|
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
data-testid="zipcode-input"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section 2: Partner Information */}
|
|
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
|
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
<Heart className="h-6 w-6 text-[#ff9e77]" />
|
|
Partner Information
|
|
</h2>
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
|
<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-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
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-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
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="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
|
/>
|
|
<Label htmlFor="partner_is_member" className="cursor-pointer text-[#422268]">
|
|
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="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
|
/>
|
|
<Label htmlFor="partner_plan_to_become_member" className="cursor-pointer text-[#422268]">
|
|
My partner plans to become a member
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section 3: Newsletter Preferences */}
|
|
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
|
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
<Mail className="h-6 w-6 text-[#81B29A]" />
|
|
Newsletter Preferences
|
|
</h2>
|
|
<p className="text-[#664fa3] mb-4" 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="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
|
/>
|
|
<Label htmlFor="newsletter_publish_name" className="cursor-pointer text-[#422268]">
|
|
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="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
|
/>
|
|
<Label htmlFor="newsletter_publish_photo" className="cursor-pointer text-[#422268]">
|
|
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="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
|
/>
|
|
<Label htmlFor="newsletter_publish_birthday" className="cursor-pointer text-[#422268]">
|
|
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="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
|
/>
|
|
<Label htmlFor="newsletter_publish_none" className="cursor-pointer text-[#422268]">
|
|
Do not publish any information
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section 4: Volunteer Interests */}
|
|
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
|
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
<Users className="h-6 w-6 text-[#664fa3]" />
|
|
Volunteer Interests
|
|
</h2>
|
|
<p className="text-[#664fa3] mb-4" 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="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
|
/>
|
|
<Label
|
|
htmlFor={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
|
|
className="cursor-pointer text-[#422268]"
|
|
>
|
|
{option}
|
|
</Label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section 5: Member Directory Settings */}
|
|
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
|
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
<BookUser className="h-6 w-6 text-[#ff9e77]" />
|
|
Member Directory Settings
|
|
</h2>
|
|
<p className="text-[#664fa3] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Control your visibility and information in the member directory.
|
|
</p>
|
|
|
|
<div className="space-y-6">
|
|
<div className="flex items-center gap-3 p-4 bg-[#f9f5ff] rounded-lg">
|
|
<input
|
|
type="checkbox"
|
|
id="show_in_directory"
|
|
name="show_in_directory"
|
|
checked={formData.show_in_directory}
|
|
onChange={handleCheckboxChange}
|
|
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
|
|
/>
|
|
<Label htmlFor="show_in_directory" className="cursor-pointer text-[#422268] font-medium">
|
|
Include me in the member directory
|
|
</Label>
|
|
</div>
|
|
|
|
{formData.show_in_directory && (
|
|
<div className="space-y-6 pl-4 border-l-4 border-[#DDD8EB]">
|
|
<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-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
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-[#ddd8eb] focus:border-[#664fa3] 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-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
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-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
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-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
/>
|
|
</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-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
|
placeholder="Optional - partner name to show in directory"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
|
|
<Button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8 py-6 text-lg font-medium shadow-lg disabled:opacity-50"
|
|
data-testid="save-profile-button"
|
|
>
|
|
<Save className="h-5 w-5 mr-2" />
|
|
{loading ? 'Saving...' : 'Save Changes'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
|
|
<ChangePasswordDialog
|
|
open={passwordDialogOpen}
|
|
onOpenChange={setPasswordDialogOpen}
|
|
/>
|
|
</div>
|
|
<MemberFooter />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Profile;
|