Merge from dev to loaf-prod for DEMO #25
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
import { Card } from '../components/ui/card';
|
import { Card } from '../components/ui/card';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
@@ -9,11 +8,10 @@ import { Label } from '../components/ui/label';
|
|||||||
import { Textarea } from '../components/ui/textarea';
|
import { Textarea } from '../components/ui/textarea';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import Navbar from '../components/Navbar';
|
import Navbar from '../components/Navbar';
|
||||||
import MemberFooter from '../components/MemberFooter';
|
import { User, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2, Eye, CreditCard, Handshake, ArrowLeft } from 'lucide-react';
|
||||||
import { User, ArrowLeft, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react';
|
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar';
|
import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar';
|
||||||
import ChangePasswordDialog from '../components/ChangePasswordDialog';
|
import ChangePasswordDialog from '../components/ChangePasswordDialog';
|
||||||
import TransactionHistory from '../components/TransactionHistory';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -24,13 +22,13 @@ const Profile = () => {
|
|||||||
const [previewImage, setPreviewImage] = useState(null);
|
const [previewImage, setPreviewImage] = useState(null);
|
||||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); // Default 50MB
|
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50);
|
||||||
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes
|
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800);
|
||||||
const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] });
|
const [activeTab, setActiveTab] = useState('account');
|
||||||
const [transactionsLoading, setTransactionsLoading] = useState(true);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
|
const [initialFormData, setInitialFormData] = useState(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
// Personal Information
|
|
||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
@@ -38,19 +36,15 @@ const Profile = () => {
|
|||||||
city: '',
|
city: '',
|
||||||
state: '',
|
state: '',
|
||||||
zipcode: '',
|
zipcode: '',
|
||||||
// Partner Information
|
|
||||||
partner_first_name: '',
|
partner_first_name: '',
|
||||||
partner_last_name: '',
|
partner_last_name: '',
|
||||||
partner_is_member: false,
|
partner_is_member: false,
|
||||||
partner_plan_to_become_member: false,
|
partner_plan_to_become_member: false,
|
||||||
// Newsletter Preferences
|
|
||||||
newsletter_publish_name: false,
|
newsletter_publish_name: false,
|
||||||
newsletter_publish_photo: false,
|
newsletter_publish_photo: false,
|
||||||
newsletter_publish_birthday: false,
|
newsletter_publish_birthday: false,
|
||||||
newsletter_publish_none: false,
|
newsletter_publish_none: false,
|
||||||
// Volunteer Interests (array)
|
|
||||||
volunteer_interests: [],
|
volunteer_interests: [],
|
||||||
// Member Directory Settings
|
|
||||||
show_in_directory: false,
|
show_in_directory: false,
|
||||||
directory_email: '',
|
directory_email: '',
|
||||||
directory_bio: '',
|
directory_bio: '',
|
||||||
@@ -63,9 +57,16 @@ const Profile = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
fetchTransactions();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Track unsaved changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialFormData) {
|
||||||
|
const hasChanges = JSON.stringify(formData) !== JSON.stringify(initialFormData);
|
||||||
|
setHasUnsavedChanges(hasChanges);
|
||||||
|
}
|
||||||
|
}, [formData, initialFormData]);
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/config');
|
const response = await api.get('/config');
|
||||||
@@ -73,7 +74,6 @@ const Profile = () => {
|
|||||||
setMaxFileSizeBytes(response.data.max_file_size_bytes);
|
setMaxFileSizeBytes(response.data.max_file_size_bytes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch config, using defaults:', 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);
|
setProfileData(response.data);
|
||||||
setProfilePhotoUrl(response.data.profile_photo_url);
|
setProfilePhotoUrl(response.data.profile_photo_url);
|
||||||
setPreviewImage(response.data.profile_photo_url);
|
setPreviewImage(response.data.profile_photo_url);
|
||||||
setFormData({
|
const newFormData = {
|
||||||
// Personal Information
|
|
||||||
first_name: response.data.first_name || '',
|
first_name: response.data.first_name || '',
|
||||||
last_name: response.data.last_name || '',
|
last_name: response.data.last_name || '',
|
||||||
phone: response.data.phone || '',
|
phone: response.data.phone || '',
|
||||||
@@ -92,19 +91,15 @@ const Profile = () => {
|
|||||||
city: response.data.city || '',
|
city: response.data.city || '',
|
||||||
state: response.data.state || '',
|
state: response.data.state || '',
|
||||||
zipcode: response.data.zipcode || '',
|
zipcode: response.data.zipcode || '',
|
||||||
// Partner Information
|
|
||||||
partner_first_name: response.data.partner_first_name || '',
|
partner_first_name: response.data.partner_first_name || '',
|
||||||
partner_last_name: response.data.partner_last_name || '',
|
partner_last_name: response.data.partner_last_name || '',
|
||||||
partner_is_member: response.data.partner_is_member || false,
|
partner_is_member: response.data.partner_is_member || false,
|
||||||
partner_plan_to_become_member: response.data.partner_plan_to_become_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_name: response.data.newsletter_publish_name || false,
|
||||||
newsletter_publish_photo: response.data.newsletter_publish_photo || false,
|
newsletter_publish_photo: response.data.newsletter_publish_photo || false,
|
||||||
newsletter_publish_birthday: response.data.newsletter_publish_birthday || false,
|
newsletter_publish_birthday: response.data.newsletter_publish_birthday || false,
|
||||||
newsletter_publish_none: response.data.newsletter_publish_none || false,
|
newsletter_publish_none: response.data.newsletter_publish_none || false,
|
||||||
// Volunteer Interests
|
|
||||||
volunteer_interests: response.data.volunteer_interests || [],
|
volunteer_interests: response.data.volunteer_interests || [],
|
||||||
// Member Directory Settings
|
|
||||||
show_in_directory: response.data.show_in_directory || false,
|
show_in_directory: response.data.show_in_directory || false,
|
||||||
directory_email: response.data.directory_email || '',
|
directory_email: response.data.directory_email || '',
|
||||||
directory_bio: response.data.directory_bio || '',
|
directory_bio: response.data.directory_bio || '',
|
||||||
@@ -112,25 +107,14 @@ const Profile = () => {
|
|||||||
directory_phone: response.data.directory_phone || '',
|
directory_phone: response.data.directory_phone || '',
|
||||||
directory_dob: response.data.directory_dob || '',
|
directory_dob: response.data.directory_dob || '',
|
||||||
directory_partner_name: response.data.directory_partner_name || ''
|
directory_partner_name: response.data.directory_partner_name || ''
|
||||||
});
|
};
|
||||||
|
setFormData(newFormData);
|
||||||
|
setInitialFormData(newFormData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to load profile');
|
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 handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
@@ -150,7 +134,6 @@ const Profile = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Volunteer interest options
|
|
||||||
const volunteerOptions = [
|
const volunteerOptions = [
|
||||||
'Event Planning',
|
'Event Planning',
|
||||||
'Social Media',
|
'Social Media',
|
||||||
@@ -168,13 +151,11 @@ const Profile = () => {
|
|||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
// Validate file type
|
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith('image/')) {
|
||||||
toast.error('Please select an image file');
|
toast.error('Please select an image file');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file size
|
|
||||||
if (file.size > maxFileSizeBytes) {
|
if (file.size > maxFileSizeBytes) {
|
||||||
toast.error(`File size must be less than ${maxFileSizeMB}MB`);
|
toast.error(`File size must be less than ${maxFileSizeMB}MB`);
|
||||||
return;
|
return;
|
||||||
@@ -222,6 +203,8 @@ const Profile = () => {
|
|||||||
try {
|
try {
|
||||||
await api.put('/users/profile', formData);
|
await api.put('/users/profile', formData);
|
||||||
toast.success('Profile updated successfully!');
|
toast.success('Profile updated successfully!');
|
||||||
|
setInitialFormData(formData);
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to update profile');
|
toast.error('Failed to update profile');
|
||||||
@@ -230,91 +213,94 @@ 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) {
|
if (!profileData) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white dark:bg-[var(--purple-deep)]">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="flex items-center justify-center min-h-[60vh]">
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p>
|
<p className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// Account & Privacy Tab Content
|
||||||
<div className="min-h-screen bg-white">
|
const AccountPrivacyContent = () => (
|
||||||
<Navbar />
|
<div className="space-y-6 ">
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
<Card className="space-y-6 px-6 pb-6">
|
||||||
<div className="mb-8 flex justify-between">
|
<div className="bg-brand-purple text-white px-4 py-3 rounded-t-xl -mx-6 -mt-6 mb-6">
|
||||||
<div classname="">
|
<h3 className="font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>Account & Privacy</h3>
|
||||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
My Profile
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
Update your personal information below.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Todo: functional back button */}
|
<div>
|
||||||
<Button
|
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Login Email</p>
|
||||||
onClick={() => navigate(-1)}
|
<p className="text-[var(--purple-ink)] dark:text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.email}</p>
|
||||||
className="h-fit bg-brand-purple hover:bg-brand-purple/80">
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="p-8 bg-white rounded-2xl border border-[var(--neutral-800)] shadow-lg">
|
<div className="flex items-center justify-between">
|
||||||
{/* Read-only Information */}
|
|
||||||
<div className="mb-8 pb-8 border-b border-[var(--neutral-800)]">
|
|
||||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
<User className="h-6 w-6 text-brand-purple " />
|
|
||||||
Account Information
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Password</p>
|
||||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.email}</p>
|
<p className="text-[var(--purple-ink)] dark:text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>••••••••</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Status</p>
|
|
||||||
<p className="text-[var(--purple-ink)] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.status.replace('_', ' ')}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Role</p>
|
|
||||||
<p className="text-[var(--purple-ink)] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.role}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Date of Birth</p>
|
|
||||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
{new Date(profileData.date_of_birth).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setPasswordDialogOpen(true)}
|
onClick={() => setPasswordDialogOpen(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-2 border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full px-6 py-3"
|
className="border-2 border-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-[var(--lavender-300)] rounded-lg px-4 py-2"
|
||||||
>
|
>
|
||||||
<Lock className="h-4 w-4 mr-2" />
|
Change
|
||||||
Change Password
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Payment Method</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CreditCard className="h-6 w-6 text-brand-purple" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="border-2 border-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-[var(--lavender-300)] rounded-lg px-4 py-2"
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Profile Photo Section */}
|
{/* Profile Photo Section */}
|
||||||
<div className="pb-8 mb-8 border-b border-[var(--neutral-800)]">
|
<div className="pb-6 border-b border-[var(--neutral-800)]">
|
||||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<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-6 w-6 text-brand-purple " />
|
<Camera className="h-5 w-5 text-brand-purple" />
|
||||||
Profile Photo
|
Profile Photo
|
||||||
</h2>
|
</h4>
|
||||||
<div className="flex flex-col md:flex-row items-center gap-6">
|
<div className="flex flex-col md:flex-row items-center gap-6">
|
||||||
<Avatar className="h-32 w-32 border-4 border-[var(--neutral-800)]">
|
<Avatar className="h-24 w-24 border-4 border-[var(--neutral-800)]">
|
||||||
<AvatarImage src={previewImage} alt="Profile" />
|
<AvatarImage src={previewImage} alt="Profile" />
|
||||||
<AvatarFallback className="bg-[var(--lavender-300)] text-brand-purple text-3xl">
|
<AvatarFallback className="bg-[var(--lavender-300)] text-brand-purple text-2xl">
|
||||||
{profileData?.first_name?.charAt(0)}{profileData?.last_name?.charAt(0)}
|
{profileData?.first_name?.charAt(0)}{profileData?.last_name?.charAt(0)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@@ -332,7 +318,7 @@ const Profile = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={uploadingPhoto}
|
disabled={uploadingPhoto}
|
||||||
className="bg-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full px-6 py-3"
|
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" />
|
<Upload className="h-4 w-4 mr-2" />
|
||||||
{uploadingPhoto ? 'Uploading...' : 'Upload Photo'}
|
{uploadingPhoto ? 'Uploading...' : 'Upload Photo'}
|
||||||
@@ -344,27 +330,27 @@ const Profile = () => {
|
|||||||
onClick={handlePhotoDelete}
|
onClick={handlePhotoDelete}
|
||||||
disabled={uploadingPhoto}
|
disabled={uploadingPhoto}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50 rounded-full px-6 py-3"
|
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" />
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
Delete Photo
|
Delete Photo
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Upload a profile photo (Max {maxFileSizeMB}MB)
|
Max {maxFileSizeMB}MB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editable Form */}
|
{/* Personal Information */}
|
||||||
<form onSubmit={handleSubmit} className="space-y-6" data-testid="profile-form">
|
<div className="space-y-4">
|
||||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h4 className="text-lg font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Personal Information
|
Personal Information
|
||||||
</h2>
|
</h4>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="first_name">First Name</Label>
|
<Label htmlFor="first_name">First Name</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -372,7 +358,7 @@ const Profile = () => {
|
|||||||
name="first_name"
|
name="first_name"
|
||||||
value={formData.first_name}
|
value={formData.first_name}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
data-testid="first-name-input"
|
data-testid="first-name-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,7 +369,7 @@ const Profile = () => {
|
|||||||
name="last_name"
|
name="last_name"
|
||||||
value={formData.last_name}
|
value={formData.last_name}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
data-testid="last-name-input"
|
data-testid="last-name-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -397,7 +383,7 @@ const Profile = () => {
|
|||||||
type="tel"
|
type="tel"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
data-testid="phone-input"
|
data-testid="phone-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -409,12 +395,12 @@ const Profile = () => {
|
|||||||
name="address"
|
name="address"
|
||||||
value={formData.address}
|
value={formData.address}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
data-testid="address-input"
|
data-testid="address-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 sm:gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="city">City</Label>
|
<Label htmlFor="city">City</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -422,7 +408,7 @@ const Profile = () => {
|
|||||||
name="city"
|
name="city"
|
||||||
value={formData.city}
|
value={formData.city}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
data-testid="city-input"
|
data-testid="city-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -433,7 +419,7 @@ const Profile = () => {
|
|||||||
name="state"
|
name="state"
|
||||||
value={formData.state}
|
value={formData.state}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
data-testid="state-input"
|
data-testid="state-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -444,20 +430,132 @@ const Profile = () => {
|
|||||||
name="zipcode"
|
name="zipcode"
|
||||||
value={formData.zipcode}
|
value={formData.zipcode}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
data-testid="zipcode-input"
|
data-testid="zipcode-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Section 2: Partner Information */}
|
{/* Member Directory Settings */}
|
||||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
|
||||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
<Heart className="h-6 w-6 text-[var(--orange-light)]" />
|
<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
|
Partner Information
|
||||||
</h2>
|
</h4>
|
||||||
<div className="space-y-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="partner_first_name">Partner First Name</Label>
|
<Label htmlFor="partner_first_name">Partner First Name</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -465,7 +563,7 @@ const Profile = () => {
|
|||||||
name="partner_first_name"
|
name="partner_first_name"
|
||||||
value={formData.partner_first_name}
|
value={formData.partner_first_name}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -476,7 +574,7 @@ const Profile = () => {
|
|||||||
name="partner_last_name"
|
name="partner_last_name"
|
||||||
value={formData.partner_last_name}
|
value={formData.partner_last_name}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -488,11 +586,10 @@ const Profile = () => {
|
|||||||
id="partner_is_member"
|
id="partner_is_member"
|
||||||
name="partner_is_member"
|
name="partner_is_member"
|
||||||
checked={formData.partner_is_member}
|
checked={formData.partner_is_member}
|
||||||
accent-color="var(--brand-white)"
|
|
||||||
onChange={handleCheckboxChange}
|
onChange={handleCheckboxChange}
|
||||||
className="ui-checkbox "
|
className="ui-checkbox"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="partner_is_member" className="cursor-pointer text-[var(--purple-ink)] ">
|
<Label htmlFor="partner_is_member" className="cursor-pointer text-[var(--purple-ink)]">
|
||||||
My partner is a current member
|
My partner is a current member
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -503,7 +600,7 @@ const Profile = () => {
|
|||||||
name="partner_plan_to_become_member"
|
name="partner_plan_to_become_member"
|
||||||
checked={formData.partner_plan_to_become_member}
|
checked={formData.partner_plan_to_become_member}
|
||||||
onChange={handleCheckboxChange}
|
onChange={handleCheckboxChange}
|
||||||
className="ui-checkbox "
|
className="ui-checkbox"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="partner_plan_to_become_member" className="cursor-pointer text-[var(--purple-ink)]">
|
<Label htmlFor="partner_plan_to_become_member" className="cursor-pointer text-[var(--purple-ink)]">
|
||||||
My partner plans to become a member
|
My partner plans to become a member
|
||||||
@@ -511,15 +608,14 @@ const Profile = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Section 3: Newsletter Preferences */}
|
{/* Newsletter Preferences */}
|
||||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
|
||||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
<Mail className="h-6 w-6 text-[var(--green-light)]" />
|
<Mail className="h-5 w-5 text-[var(--green-light)]" />
|
||||||
Newsletter Preferences
|
Newsletter Preferences
|
||||||
</h2>
|
</h4>
|
||||||
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-brand-purple text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Choose what information you'd like published in our member newsletter.
|
Choose what information you'd like published in our member newsletter.
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -530,7 +626,7 @@ const Profile = () => {
|
|||||||
name="newsletter_publish_name"
|
name="newsletter_publish_name"
|
||||||
checked={formData.newsletter_publish_name}
|
checked={formData.newsletter_publish_name}
|
||||||
onChange={handleCheckboxChange}
|
onChange={handleCheckboxChange}
|
||||||
className="ui-checkbox "
|
className="ui-checkbox"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="newsletter_publish_name" className="cursor-pointer text-[var(--purple-ink)]">
|
<Label htmlFor="newsletter_publish_name" className="cursor-pointer text-[var(--purple-ink)]">
|
||||||
Publish my name
|
Publish my name
|
||||||
@@ -578,13 +674,13 @@ const Profile = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section 4: Volunteer Interests */}
|
{/* Volunteer Interests */}
|
||||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
|
||||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
<Users className="h-6 w-6 text-brand-purple " />
|
<Users className="h-5 w-5 text-brand-purple" />
|
||||||
Volunteer Interests
|
Volunteer Interests
|
||||||
</h2>
|
</h4>
|
||||||
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<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.
|
Select areas where you'd like to volunteer and help our community.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid md:grid-cols-2 gap-3">
|
<div className="grid md:grid-cols-2 gap-3">
|
||||||
@@ -595,7 +691,7 @@ const Profile = () => {
|
|||||||
id={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
|
id={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
|
||||||
checked={formData.volunteer_interests.includes(option)}
|
checked={formData.volunteer_interests.includes(option)}
|
||||||
onChange={() => handleVolunteerToggle(option)}
|
onChange={() => handleVolunteerToggle(option)}
|
||||||
className="ui-checkbox "
|
className="ui-checkbox"
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
|
htmlFor={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
|
||||||
@@ -607,133 +703,133 @@ const Profile = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
{/* Section 5: Member Directory Settings */}
|
return (
|
||||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
<div className="min-h-screen bg-background flex flex-col">
|
||||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<Navbar />
|
||||||
<BookUser className="h-6 w-6 text-[var(--orange-light)]" />
|
|
||||||
Member Directory Settings
|
|
||||||
</h2>
|
|
||||||
<p className="text-brand-purple 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-1 flex flex-col">
|
||||||
<div className="flex items-center gap-3 p-4 bg-[var(--lavender-400)] rounded-lg">
|
<div className="max-w-5xl mx-auto px-4 sm:px-6 py-8 w-full flex-1 pb-24">
|
||||||
<input
|
{/* Header */}
|
||||||
type="checkbox"
|
<div className="flex items-center justify-between mb-6">
|
||||||
id="show_in_directory"
|
<div className='space-y-4'>
|
||||||
name="show_in_directory"
|
|
||||||
checked={formData.show_in_directory}
|
<h1 className="text-4xl md:text-4xl font-semibold " style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
onChange={handleCheckboxChange}
|
My Profile
|
||||||
className="ui-checkbox"
|
</h1>
|
||||||
/>
|
<p className='text-brand-purple text-md'>Update your personal information below.</p>
|
||||||
<Label htmlFor="show_in_directory" className="cursor-pointer text-[var(--purple-ink)] font-medium">
|
</div>
|
||||||
Include me in the member directory
|
{/* <Button
|
||||||
</Label>
|
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>
|
</div>
|
||||||
|
|
||||||
{formData.show_in_directory && (
|
{/* Main Content Div */}
|
||||||
<div className="space-y-6 pl-4 border-l-4 border-[var(--neutral-800)]">
|
<div className="overflow-hidden ">
|
||||||
<div>
|
<form onSubmit={handleSubmit} data-testid="profile-form">
|
||||||
<Label htmlFor="directory_email">Directory Email</Label>
|
{/* Mobile Tabs */}
|
||||||
<Input
|
<div className="md:hidden flex border-b border-[var(--neutral-800)] mb-4 gap-1 ">
|
||||||
id="directory_email"
|
{tabs.map((tab) => {
|
||||||
name="directory_email"
|
const IconComponent = tab.icon;
|
||||||
type="email"
|
return (
|
||||||
value={formData.directory_email}
|
<button
|
||||||
onChange={handleInputChange}
|
key={tab.id}
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
type="button"
|
||||||
placeholder="Optional - email to show in directory"
|
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>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* Desktop Layout */}
|
||||||
<Label htmlFor="directory_bio">Bio</Label>
|
<div className="flex">
|
||||||
<Textarea
|
{/* Desktop Sidebar Tabs */}
|
||||||
id="directory_bio"
|
<div className="hidden md:flex flex-col w-64 border-[var(--neutral-800)] mr-4 gap-2">
|
||||||
name="directory_bio"
|
{tabs.map((tab) => {
|
||||||
value={formData.directory_bio}
|
const IconComponent = tab.icon;
|
||||||
onChange={handleInputChange}
|
return (
|
||||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple min-h-[100px]"
|
<button
|
||||||
placeholder="Tell other members about yourself..."
|
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>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* Content Area */}
|
||||||
<Label htmlFor="directory_address">Address</Label>
|
<div className="flex-1 p-6 min-h-[500px]">
|
||||||
<Input
|
{activeTab === 'account' && <AccountPrivacyContent />}
|
||||||
id="directory_address"
|
{activeTab === 'bio' && <BioDirectoryContent />}
|
||||||
name="directory_address"
|
{activeTab === 'engagement' && <EngagementContent />}
|
||||||
value={formData.directory_address}
|
</div>
|
||||||
onChange={handleInputChange}
|
</div>
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
</form>
|
||||||
placeholder="Optional - address to show in directory"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* Sticky Footer */}
|
||||||
<Label htmlFor="directory_phone">Phone</Label>
|
<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">
|
||||||
<Input
|
<div className="max-w-5xl px-6 mx-auto flex items-center justify-between">
|
||||||
id="directory_phone"
|
|
||||||
name="directory_phone"
|
|
||||||
type="tel"
|
|
||||||
value={formData.directory_phone}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
|
||||||
placeholder="Optional - phone to show in directory"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div className='flex gap-2 w-full lg:justify-between md:mr-5'>
|
||||||
<Label htmlFor="directory_dob">Date of Birth</Label>
|
<Button
|
||||||
<Input
|
onClick={() => navigate(-1)}
|
||||||
id="directory_dob"
|
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">
|
||||||
name="directory_dob"
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
type="date"
|
Back
|
||||||
value={formData.directory_dob}
|
</Button>
|
||||||
onChange={handleInputChange}
|
<div className="flex items-center gap-2">
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
{hasUnsavedChanges && (
|
||||||
/>
|
<>
|
||||||
</div>
|
<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" }}>
|
||||||
<div>
|
Unsaved changes
|
||||||
<Label htmlFor="directory_partner_name">Partner Name</Label>
|
</span>
|
||||||
<Input
|
</>
|
||||||
id="directory_partner_name"
|
|
||||||
name="directory_partner_name"
|
|
||||||
value={formData.directory_partner_name}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
|
||||||
placeholder="Optional - partner name to show in directory"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="bg-brand-purple text-white hover:bg-brand-dark-lavender rounded-full px-8 py-6 text-lg font-medium shadow-lg disabled:opacity-50"
|
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"
|
data-testid="save-profile-button"
|
||||||
>
|
>
|
||||||
<Save className="h-5 w-5 mr-2" />
|
|
||||||
{loading ? 'Saving...' : 'Save Changes'}
|
{loading ? 'Saving...' : 'Save Changes'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</Card>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ChangePasswordDialog
|
<ChangePasswordDialog
|
||||||
open={passwordDialogOpen}
|
open={passwordDialogOpen}
|
||||||
onOpenChange={setPasswordDialogOpen}
|
onOpenChange={setPasswordDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
|
||||||
<MemberFooter />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user