2026-01-20 12:37:22 +05:30
|
|
|
'use client';
|
|
|
|
|
|
2026-02-01 00:35:22 +05:30
|
|
|
import { useState, useRef, useEffect } from 'react';
|
2026-01-20 12:37:22 +05:30
|
|
|
import Image from 'next/image';
|
2026-02-01 00:35:22 +05:30
|
|
|
import { useSession, signIn } from 'next-auth/react';
|
|
|
|
|
import { uploadService } from '@/services/upload.service';
|
|
|
|
|
import { agentsService } from '@/services/agents.service';
|
2026-02-01 00:50:09 +05:30
|
|
|
import { usersService } from '@/services/users.service';
|
2026-04-20 14:07:33 +05:30
|
|
|
import { authService, AuthService } from '@/services/auth.service';
|
2026-01-20 12:37:22 +05:30
|
|
|
|
|
|
|
|
interface ProfileSettingsFormProps {
|
|
|
|
|
initialData?: {
|
2026-02-01 00:59:37 +05:30
|
|
|
firstName: string;
|
|
|
|
|
lastName: string;
|
2026-01-20 12:37:22 +05:30
|
|
|
career: string;
|
|
|
|
|
email: string;
|
|
|
|
|
phone: string;
|
2026-02-01 00:35:22 +05:30
|
|
|
avatar?: string;
|
2026-01-20 12:37:22 +05:30
|
|
|
};
|
|
|
|
|
onSave?: (data: {
|
2026-02-01 00:59:37 +05:30
|
|
|
firstName: string;
|
|
|
|
|
lastName: string;
|
2026-01-20 12:37:22 +05:30
|
|
|
career: string;
|
|
|
|
|
email: string;
|
|
|
|
|
phone: string;
|
|
|
|
|
}) => void;
|
|
|
|
|
descriptionText?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ProfileSettingsForm({
|
2026-02-01 00:35:22 +05:30
|
|
|
initialData,
|
2026-01-20 12:37:22 +05:30
|
|
|
onSave,
|
|
|
|
|
descriptionText = 'This information will be displayed on your public profile page.',
|
|
|
|
|
}: ProfileSettingsFormProps) {
|
2026-02-01 00:35:22 +05:30
|
|
|
const { data: session, update: updateSession } = useSession();
|
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
const [formData, setFormData] = useState({
|
2026-02-01 00:59:37 +05:30
|
|
|
firstName: initialData?.firstName || '',
|
|
|
|
|
lastName: initialData?.lastName || '',
|
2026-02-01 00:35:22 +05:30
|
|
|
career: initialData?.career || '',
|
|
|
|
|
email: initialData?.email || '',
|
|
|
|
|
phone: initialData?.phone || '',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
2026-03-13 22:45:11 +05:30
|
|
|
const [avatarLoaded, setAvatarLoaded] = useState(false);
|
2026-02-01 00:35:22 +05:30
|
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
|
|
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
2026-04-20 14:07:33 +05:30
|
|
|
const [isEmailModalOpen, setIsEmailModalOpen] = useState(false);
|
|
|
|
|
const [emailModalData, setEmailModalData] = useState({ newEmail: '', password: '' });
|
|
|
|
|
const [emailModalError, setEmailModalError] = useState<string | null>(null);
|
|
|
|
|
const [emailModalSubmitting, setEmailModalSubmitting] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleRequestEmailChange = async () => {
|
|
|
|
|
setEmailModalError(null);
|
|
|
|
|
if (!emailModalData.newEmail.trim() || !emailModalData.password) {
|
|
|
|
|
setEmailModalError('Please enter a new email and your current password');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setEmailModalSubmitting(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await authService.requestEmailChange({
|
|
|
|
|
newEmail: emailModalData.newEmail.trim(),
|
|
|
|
|
password: emailModalData.password,
|
|
|
|
|
});
|
|
|
|
|
setSuccessMessage(res.data?.message || 'Verification email sent. Please check your new inbox.');
|
|
|
|
|
setIsEmailModalOpen(false);
|
|
|
|
|
setEmailModalData({ newEmail: '', password: '' });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setEmailModalError(AuthService.getErrorMessage(err));
|
|
|
|
|
} finally {
|
|
|
|
|
setEmailModalSubmitting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-03-13 22:45:11 +05:30
|
|
|
// Reset avatar loaded state when URL changes (e.g., after upload)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setAvatarLoaded(false);
|
|
|
|
|
}, [avatarUrl]);
|
|
|
|
|
|
2026-02-09 02:10:13 +05:30
|
|
|
// Store original data for cancel/reset functionality
|
|
|
|
|
const [originalData, setOriginalData] = useState<typeof formData | null>(null);
|
2026-03-04 21:40:16 +05:30
|
|
|
// Track if initial profile fetch is done to avoid re-showing loading spinner
|
|
|
|
|
const hasFetchedRef = useRef(false);
|
2026-02-01 00:35:22 +05:30
|
|
|
|
2026-04-24 12:56:20 +05:30
|
|
|
const fetchProfile = async () => {
|
|
|
|
|
try {
|
|
|
|
|
// Only show loading spinner on initial fetch, not on session-triggered re-fetches
|
|
|
|
|
if (!hasFetchedRef.current) {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
}
|
|
|
|
|
const role = (session?.user as any)?.role;
|
|
|
|
|
|
|
|
|
|
if (role === 'AGENT') {
|
|
|
|
|
const profile = await agentsService.getMyProfile();
|
|
|
|
|
const profileData = {
|
|
|
|
|
firstName: profile.firstName || '',
|
|
|
|
|
lastName: profile.lastName || '',
|
|
|
|
|
career: profile.headline || profile.agentType?.name || 'Real Estate Agent',
|
|
|
|
|
email: profile.email || session?.user?.email || '',
|
|
|
|
|
phone: profile.phone || '',
|
|
|
|
|
};
|
|
|
|
|
setFormData(profileData);
|
|
|
|
|
setOriginalData(profileData);
|
|
|
|
|
|
|
|
|
|
// If backend email diverges from session (email change verified in another tab),
|
|
|
|
|
// refresh the next-auth JWT so header/other places also pick up the new address.
|
|
|
|
|
if (profile.email && session?.user?.email && profile.email !== session.user.email) {
|
|
|
|
|
try {
|
|
|
|
|
await updateSession({ user: { email: profile.email } });
|
|
|
|
|
} catch {
|
|
|
|
|
// updateSession is best-effort
|
|
|
|
|
}
|
2026-03-04 21:40:16 +05:30
|
|
|
}
|
2026-04-24 12:56:20 +05:30
|
|
|
|
|
|
|
|
if (profile.avatar) {
|
|
|
|
|
try {
|
|
|
|
|
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar);
|
|
|
|
|
setAvatarUrl(presignedUrl);
|
|
|
|
|
} catch {
|
|
|
|
|
setAvatarUrl(profile.avatar);
|
2026-02-01 00:50:09 +05:30
|
|
|
}
|
2026-04-24 12:56:20 +05:30
|
|
|
}
|
|
|
|
|
} else if (role === 'USER') {
|
|
|
|
|
const profile = await usersService.getMyProfile();
|
|
|
|
|
const profileData = {
|
|
|
|
|
firstName: profile.firstName || '',
|
|
|
|
|
lastName: profile.lastName || '',
|
|
|
|
|
career: '',
|
|
|
|
|
email: profile.email || session?.user?.email || '',
|
|
|
|
|
phone: profile.phone || '',
|
|
|
|
|
};
|
|
|
|
|
setFormData(profileData);
|
|
|
|
|
setOriginalData(profileData);
|
|
|
|
|
|
|
|
|
|
if (profile.email && session?.user?.email && profile.email !== session.user.email) {
|
|
|
|
|
try {
|
|
|
|
|
await updateSession({ user: { email: profile.email } });
|
|
|
|
|
} catch {
|
|
|
|
|
// best-effort
|
2026-02-01 00:35:22 +05:30
|
|
|
}
|
|
|
|
|
}
|
2026-04-24 12:56:20 +05:30
|
|
|
|
|
|
|
|
if (profile.avatar) {
|
|
|
|
|
try {
|
|
|
|
|
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar);
|
|
|
|
|
setAvatarUrl(presignedUrl);
|
|
|
|
|
} catch {
|
|
|
|
|
setAvatarUrl(profile.avatar);
|
2026-02-01 00:35:22 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-24 12:56:20 +05:30
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to fetch profile:', err);
|
|
|
|
|
if (session?.user) {
|
|
|
|
|
const nameParts = (session.user?.name || '').split(' ');
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
firstName: nameParts[0] || prev.firstName,
|
|
|
|
|
lastName: nameParts.slice(1).join(' ') || prev.lastName,
|
|
|
|
|
email: session.user?.email || prev.email,
|
|
|
|
|
}));
|
|
|
|
|
if (session.user.image) {
|
|
|
|
|
setAvatarUrl(session.user.image);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
hasFetchedRef.current = true;
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-01 00:35:22 +05:30
|
|
|
|
2026-04-24 12:56:20 +05:30
|
|
|
// Initial fetch on mount
|
|
|
|
|
useEffect(() => {
|
2026-02-01 00:35:22 +05:30
|
|
|
if (session) {
|
2026-03-04 21:40:16 +05:30
|
|
|
if (hasFetchedRef.current) return;
|
2026-02-01 00:35:22 +05:30
|
|
|
fetchProfile();
|
|
|
|
|
}
|
2026-04-24 12:56:20 +05:30
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [session]);
|
|
|
|
|
|
|
|
|
|
// Re-fetch when the tab becomes visible again (e.g. user verified email
|
|
|
|
|
// change in another tab and returned). Ensures email/phone/avatar stay in
|
|
|
|
|
// sync with the source of truth.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onVisible = () => {
|
|
|
|
|
if (document.visibilityState === 'visible' && session && hasFetchedRef.current) {
|
|
|
|
|
fetchProfile();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('visibilitychange', onVisible);
|
|
|
|
|
return () => document.removeEventListener('visibilitychange', onVisible);
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-02-01 00:35:22 +05:30
|
|
|
}, [session]);
|
2026-01-20 12:37:22 +05:30
|
|
|
|
|
|
|
|
const handleChange = (field: string, value: string) => {
|
|
|
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
2026-02-01 00:35:22 +05:30
|
|
|
setError(null);
|
|
|
|
|
setSuccessMessage(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUploadClick = () => {
|
|
|
|
|
fileInputRef.current?.click();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
const file = event.target.files?.[0];
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
// Validate file type
|
|
|
|
|
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
|
|
|
|
if (!validTypes.includes(file.type)) {
|
|
|
|
|
setError('Please upload a JPEG, PNG, or GIF image.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate file size (2MB max)
|
|
|
|
|
const maxSize = 2 * 1024 * 1024;
|
|
|
|
|
if (file.size > maxSize) {
|
|
|
|
|
setError('File size must be less than 2MB.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setIsUploading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
setUploadProgress(0);
|
|
|
|
|
|
2026-02-01 00:50:09 +05:30
|
|
|
// Show local preview immediately for instant feedback
|
|
|
|
|
const localPreviewUrl = URL.createObjectURL(file);
|
|
|
|
|
setAvatarUrl(localPreviewUrl);
|
2026-02-01 00:35:22 +05:30
|
|
|
|
2026-02-01 00:50:09 +05:30
|
|
|
const role = (session?.user as any)?.role;
|
2026-02-01 00:35:22 +05:30
|
|
|
|
2026-02-01 00:50:09 +05:30
|
|
|
// Upload the avatar based on role
|
|
|
|
|
let uploadedFile;
|
|
|
|
|
if (role === 'AGENT') {
|
|
|
|
|
uploadedFile = await uploadService.uploadAvatar(file, (progress) => {
|
|
|
|
|
setUploadProgress(progress.percentage);
|
|
|
|
|
});
|
|
|
|
|
// Update the agent profile with the new avatar key
|
|
|
|
|
await agentsService.updateProfile({ avatar: uploadedFile.url });
|
|
|
|
|
} else {
|
|
|
|
|
uploadedFile = await uploadService.uploadUserAvatar(file, (progress) => {
|
|
|
|
|
setUploadProgress(progress.percentage);
|
|
|
|
|
});
|
|
|
|
|
// Update the user profile with the new avatar key
|
|
|
|
|
await usersService.updateProfile({ avatar: uploadedFile.url });
|
|
|
|
|
}
|
2026-02-01 00:35:22 +05:30
|
|
|
|
2026-02-01 00:50:09 +05:30
|
|
|
// Small delay to ensure S3 has processed the upload
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
|
|
|
|
|
|
// Get the presigned URL for display
|
|
|
|
|
try {
|
|
|
|
|
const presignedUrl = await uploadService.getPresignedDownloadUrl(uploadedFile.url);
|
|
|
|
|
setAvatarUrl(presignedUrl);
|
|
|
|
|
// Clean up local preview URL only after successful presigned URL
|
|
|
|
|
URL.revokeObjectURL(localPreviewUrl);
|
|
|
|
|
|
2026-02-01 00:59:37 +05:30
|
|
|
// Dispatch event to notify header to refresh profile data
|
|
|
|
|
window.dispatchEvent(new Event('profile-updated'));
|
2026-02-01 00:50:09 +05:30
|
|
|
} catch {
|
|
|
|
|
// If presigned URL fails, keep the local preview (don't revoke it)
|
|
|
|
|
console.warn('Could not get presigned URL, using local preview');
|
2026-02-09 00:03:45 +05:30
|
|
|
// Still dispatch event to refresh header
|
|
|
|
|
window.dispatchEvent(new Event('profile-updated'));
|
2026-02-01 00:50:09 +05:30
|
|
|
}
|
2026-02-01 00:35:22 +05:30
|
|
|
|
|
|
|
|
setSuccessMessage('Profile picture updated successfully!');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Upload failed:', err);
|
|
|
|
|
setError('Failed to upload profile picture. Please try again.');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsUploading(false);
|
|
|
|
|
setUploadProgress(0);
|
|
|
|
|
// Reset file input
|
|
|
|
|
if (fileInputRef.current) {
|
|
|
|
|
fileInputRef.current.value = '';
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-20 12:37:22 +05:30
|
|
|
};
|
|
|
|
|
|
2026-02-01 00:35:22 +05:30
|
|
|
const handleDeleteAvatar = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setIsUploading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
2026-02-01 00:50:09 +05:30
|
|
|
const role = (session?.user as any)?.role;
|
|
|
|
|
|
2026-02-01 00:59:37 +05:30
|
|
|
// Get current avatar key from profile to delete from S3
|
|
|
|
|
let avatarKey: string | null = null;
|
|
|
|
|
if (role === 'AGENT') {
|
|
|
|
|
const profile = await agentsService.getMyProfile();
|
|
|
|
|
avatarKey = profile.avatar;
|
|
|
|
|
} else {
|
|
|
|
|
const profile = await usersService.getMyProfile();
|
|
|
|
|
avatarKey = profile.avatar;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete from S3 if avatar exists
|
|
|
|
|
if (avatarKey) {
|
|
|
|
|
try {
|
|
|
|
|
await uploadService.deleteFile(avatarKey);
|
|
|
|
|
} catch (s3Error) {
|
|
|
|
|
console.warn('Could not delete from S3:', s3Error);
|
|
|
|
|
// Continue even if S3 delete fails - still clear database reference
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update profile to remove avatar reference from database
|
2026-02-01 00:50:09 +05:30
|
|
|
if (role === 'AGENT') {
|
|
|
|
|
await agentsService.updateProfile({ avatar: null });
|
|
|
|
|
} else {
|
|
|
|
|
await usersService.updateProfile({ avatar: null });
|
|
|
|
|
}
|
2026-02-01 00:35:22 +05:30
|
|
|
setAvatarUrl(null);
|
|
|
|
|
|
2026-02-01 00:59:37 +05:30
|
|
|
// Dispatch event to notify header to refresh profile data
|
|
|
|
|
window.dispatchEvent(new Event('profile-updated'));
|
|
|
|
|
|
2026-02-01 00:35:22 +05:30
|
|
|
setSuccessMessage('Profile picture removed successfully!');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Delete failed:', err);
|
|
|
|
|
setError('Failed to remove profile picture. Please try again.');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsUploading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
2026-02-01 00:50:09 +05:30
|
|
|
const role = (session?.user as any)?.role;
|
|
|
|
|
|
|
|
|
|
// Update profile based on role (email is not editable - tied to auth)
|
|
|
|
|
if (role === 'AGENT') {
|
|
|
|
|
await agentsService.updateProfile({
|
2026-02-01 00:59:37 +05:30
|
|
|
firstName: formData.firstName,
|
|
|
|
|
lastName: formData.lastName,
|
2026-02-01 00:50:09 +05:30
|
|
|
phone: formData.phone,
|
2026-03-18 13:04:10 +05:30
|
|
|
headline: formData.career,
|
2026-02-01 00:50:09 +05:30
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await usersService.updateProfile({
|
2026-02-01 00:59:37 +05:30
|
|
|
firstName: formData.firstName,
|
|
|
|
|
lastName: formData.lastName,
|
2026-02-01 00:50:09 +05:30
|
|
|
phone: formData.phone,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-01 00:35:22 +05:30
|
|
|
|
2026-02-01 00:59:37 +05:30
|
|
|
// Dispatch event to notify header and other components to refresh profile data
|
2026-03-20 12:43:23 +05:30
|
|
|
// (Don't call updateSession as it can trigger a full page reload in next-auth v5)
|
2026-02-01 00:59:37 +05:30
|
|
|
window.dispatchEvent(new Event('profile-updated'));
|
|
|
|
|
|
2026-02-01 00:35:22 +05:30
|
|
|
if (onSave) {
|
|
|
|
|
onSave(formData);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 02:10:13 +05:30
|
|
|
// Update original data so Cancel will reset to the newly saved values
|
|
|
|
|
setOriginalData(formData);
|
2026-02-01 00:35:22 +05:30
|
|
|
setSuccessMessage('Profile settings saved successfully!');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Save failed:', err);
|
|
|
|
|
setError('Failed to save profile settings. Please try again.');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
2026-01-20 12:37:22 +05:30
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-20 12:53:13 +05:30
|
|
|
const hasChanges = originalData
|
|
|
|
|
? JSON.stringify(formData) !== JSON.stringify(originalData)
|
|
|
|
|
: false;
|
|
|
|
|
|
2026-01-20 12:37:22 +05:30
|
|
|
const handleCancel = () => {
|
2026-03-20 12:53:13 +05:30
|
|
|
if (!hasChanges) return;
|
|
|
|
|
// Reset to last saved data
|
2026-02-09 02:10:13 +05:30
|
|
|
if (originalData) {
|
|
|
|
|
setFormData(originalData);
|
2026-02-01 00:35:22 +05:30
|
|
|
}
|
|
|
|
|
setError(null);
|
|
|
|
|
setSuccessMessage(null);
|
2026-01-20 12:37:22 +05:30
|
|
|
};
|
|
|
|
|
|
2026-02-01 00:35:22 +05:30
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="border border-[#00293d]/20 rounded-[15px] bg-white p-8 flex items-center justify-center min-h-[400px]">
|
|
|
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#e58625]"></div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 12:37:22 +05:30
|
|
|
return (
|
|
|
|
|
<div className="border border-[#00293d]/20 rounded-[15px] bg-white p-8">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<h1 className="font-fractul font-bold text-[20px] leading-[24px] text-[#00293D] mb-2">
|
|
|
|
|
Public Profile
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="font-serif text-[13px] leading-[17px] text-[#00293D]/60">
|
|
|
|
|
{descriptionText}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-01 00:35:22 +05:30
|
|
|
{/* Error/Success Messages */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-[10px] text-red-600 text-sm font-serif">
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{successMessage && (
|
|
|
|
|
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-[10px] text-green-600 text-sm font-serif">
|
|
|
|
|
{successMessage}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-20 12:37:22 +05:30
|
|
|
{/* Profile Photo Section */}
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
<div className="flex items-start gap-4">
|
2026-03-13 22:45:11 +05:30
|
|
|
<div className="w-[60px] h-[60px] rounded-full overflow-hidden border border-[#00293D]/20 flex-shrink-0 bg-gray-100 relative">
|
|
|
|
|
{(!avatarUrl || !avatarLoaded) && (
|
2026-03-14 14:37:19 +05:30
|
|
|
<div className="absolute inset-0 rounded-full shimmer-loading" />
|
2026-03-13 22:45:11 +05:30
|
|
|
)}
|
|
|
|
|
{avatarUrl && (
|
|
|
|
|
<img
|
2026-03-18 11:54:40 +05:30
|
|
|
ref={(el) => { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }}
|
2026-03-13 22:45:11 +05:30
|
|
|
src={avatarUrl}
|
|
|
|
|
alt="Profile"
|
2026-03-14 14:37:19 +05:30
|
|
|
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
|
2026-03-13 22:45:11 +05:30
|
|
|
onLoad={() => setAvatarLoaded(true)}
|
2026-02-01 00:35:22 +05:30
|
|
|
/>
|
|
|
|
|
)}
|
2026-01-20 12:37:22 +05:30
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-serif font-medium text-[14px] text-[#00293D] mb-2">
|
|
|
|
|
Public Picture
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
2026-02-01 00:35:22 +05:30
|
|
|
<input
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
type="file"
|
|
|
|
|
accept="image/jpeg,image/png,image/gif"
|
|
|
|
|
onChange={handleFileChange}
|
|
|
|
|
className="hidden"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleUploadClick}
|
|
|
|
|
disabled={isUploading}
|
|
|
|
|
className="px-4 py-1.5 bg-[#e58625] text-white rounded-[20px] font-serif text-[12px] hover:bg-[#d47920] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
>
|
|
|
|
|
{isUploading ? `Uploading ${uploadProgress}%` : 'Upload Now'}
|
2026-01-20 12:37:22 +05:30
|
|
|
</button>
|
2026-02-01 00:35:22 +05:30
|
|
|
<button
|
|
|
|
|
onClick={handleDeleteAvatar}
|
|
|
|
|
disabled={isUploading || !avatarUrl}
|
|
|
|
|
className="px-4 py-1.5 border border-[#00293D]/30 text-[#00293D] rounded-[20px] font-serif text-[12px] hover:bg-[#00293d]/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
>
|
2026-01-20 12:37:22 +05:30
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="font-serif text-[11px] text-[#00293D]/50">
|
|
|
|
|
Supported formats: JPEG, PNG, GIF Max file size: 2MB
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Form Fields */}
|
|
|
|
|
<div className="space-y-5">
|
2026-02-01 00:59:37 +05:30
|
|
|
{/* Row 1: First Name & Last Name */}
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
2026-01-20 12:37:22 +05:30
|
|
|
<div>
|
2026-03-28 20:08:23 +05:30
|
|
|
<label className="block text-[12px] font-bold text-[#00293D] font-serif mb-2">
|
2026-02-01 00:59:37 +05:30
|
|
|
First Name
|
2026-01-20 12:37:22 +05:30
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
2026-02-01 00:59:37 +05:30
|
|
|
value={formData.firstName}
|
|
|
|
|
onChange={(e) => handleChange('firstName', e.target.value)}
|
|
|
|
|
className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
2026-03-28 20:08:23 +05:30
|
|
|
<label className="block text-[12px] font-bold text-[#00293D] font-serif mb-2">
|
2026-02-01 00:59:37 +05:30
|
|
|
Last Name
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={formData.lastName}
|
|
|
|
|
onChange={(e) => handleChange('lastName', e.target.value)}
|
2026-01-20 12:37:22 +05:30
|
|
|
className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-28 15:38:42 +05:30
|
|
|
{/* Career (only for agents, read-only - set during onboarding) */}
|
2026-02-01 00:59:37 +05:30
|
|
|
{(session?.user as any)?.role === 'AGENT' && (
|
|
|
|
|
<div>
|
2026-03-28 20:08:23 +05:30
|
|
|
<label className="block text-[12px] font-bold text-[#00293D] font-serif mb-2">
|
2026-02-01 00:59:37 +05:30
|
|
|
Career
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={formData.career}
|
2026-03-28 15:38:42 +05:30
|
|
|
disabled
|
|
|
|
|
className="w-full sm:w-1/2 h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D]/60 bg-gray-50 cursor-not-allowed"
|
2026-02-01 00:59:37 +05:30
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-20 12:37:22 +05:30
|
|
|
{/* Row 2: Email & Phone */}
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
|
|
|
|
<div>
|
2026-03-28 20:08:23 +05:30
|
|
|
<label className="block text-[12px] font-bold text-[#00293D] font-serif mb-2">
|
2026-01-20 12:37:22 +05:30
|
|
|
Email Address
|
|
|
|
|
</label>
|
2026-04-20 14:07:33 +05:30
|
|
|
<div className="relative">
|
|
|
|
|
<input
|
|
|
|
|
type="email"
|
|
|
|
|
value={formData.email}
|
|
|
|
|
disabled
|
|
|
|
|
className="w-full h-[44px] pl-4 pr-[88px] border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D]/60 bg-gray-50 cursor-not-allowed"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setEmailModalError(null);
|
|
|
|
|
setEmailModalData({ newEmail: '', password: '' });
|
|
|
|
|
setIsEmailModalOpen(true);
|
|
|
|
|
}}
|
|
|
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1.5 text-[12px] font-serif font-semibold text-[#e58625] hover:bg-[#e58625]/10 rounded-[6px] transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Change
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-01-20 12:37:22 +05:30
|
|
|
</div>
|
|
|
|
|
<div>
|
2026-03-28 20:08:23 +05:30
|
|
|
<label className="block text-[12px] font-bold text-[#00293D] font-serif mb-2">
|
2026-01-20 12:37:22 +05:30
|
|
|
Phone Number
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="tel"
|
|
|
|
|
value={formData.phone}
|
|
|
|
|
onChange={(e) => handleChange('phone', e.target.value)}
|
|
|
|
|
className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Action Buttons */}
|
|
|
|
|
<div className="mt-8 flex justify-end gap-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleCancel}
|
2026-03-20 12:53:13 +05:30
|
|
|
disabled={isSaving || !hasChanges}
|
|
|
|
|
className="px-8 py-3 border border-[#00293D]/30 text-[#00293D] rounded-[10px] font-serif font-medium text-[14px] hover:bg-[#00293d]/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
2026-01-20 12:37:22 +05:30
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleSave}
|
2026-02-01 00:35:22 +05:30
|
|
|
disabled={isSaving}
|
|
|
|
|
className="px-8 py-3 bg-[#e58625] text-white rounded-[10px] font-serif font-medium text-[14px] hover:bg-[#d47920] transition-colors disabled:opacity-50"
|
2026-01-20 12:37:22 +05:30
|
|
|
>
|
2026-02-01 00:35:22 +05:30
|
|
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
2026-01-20 12:37:22 +05:30
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-04-20 14:07:33 +05:30
|
|
|
|
|
|
|
|
{/* Change Email Modal */}
|
|
|
|
|
{isEmailModalOpen && (
|
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
|
|
|
<div
|
|
|
|
|
className="absolute inset-0 bg-black/30"
|
|
|
|
|
onClick={() => !emailModalSubmitting && setIsEmailModalOpen(false)}
|
|
|
|
|
/>
|
|
|
|
|
<div className="relative bg-white rounded-[20px] w-full max-w-[480px] shadow-xl flex flex-col">
|
|
|
|
|
<div className="px-6 py-4 border-b border-[#d9d9d9] flex items-center justify-between">
|
|
|
|
|
<h2 className="font-fractul font-bold text-[18px] text-[#00293d]">Change Email Address</h2>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => !emailModalSubmitting && setIsEmailModalOpen(false)}
|
|
|
|
|
className="text-[#00293d] hover:opacity-70"
|
|
|
|
|
>
|
|
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
|
|
|
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="px-6 py-5 space-y-4">
|
|
|
|
|
<p className="font-serif text-[13px] text-[#00293d]/70">
|
|
|
|
|
A verification link will be sent to your new email. Your email changes only after you click the link.
|
|
|
|
|
</p>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[12px] font-bold text-[#00293D] font-serif mb-2">New Email</label>
|
|
|
|
|
<input
|
|
|
|
|
type="email"
|
|
|
|
|
value={emailModalData.newEmail}
|
|
|
|
|
onChange={(e) => setEmailModalData({ ...emailModalData, newEmail: e.target.value })}
|
|
|
|
|
placeholder="new@example.com"
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[12px] font-bold text-[#00293D] font-serif mb-2">Current Password</label>
|
|
|
|
|
<input
|
|
|
|
|
type="password"
|
|
|
|
|
value={emailModalData.password}
|
|
|
|
|
onChange={(e) => setEmailModalData({ ...emailModalData, password: e.target.value })}
|
|
|
|
|
placeholder="Enter your current password"
|
|
|
|
|
autoComplete="current-password"
|
|
|
|
|
className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{emailModalError && (
|
|
|
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-[10px]">
|
|
|
|
|
<p className="text-red-700 text-[13px] font-serif">{emailModalError}</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="px-6 py-4 border-t border-[#d9d9d9] flex items-center justify-end gap-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setIsEmailModalOpen(false)}
|
|
|
|
|
disabled={emailModalSubmitting}
|
|
|
|
|
className="px-5 py-2.5 border border-[#00293D]/30 text-[#00293D] rounded-[10px] font-serif font-medium text-[13px] hover:bg-[#00293d]/5 transition-colors disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleRequestEmailChange}
|
|
|
|
|
disabled={emailModalSubmitting}
|
|
|
|
|
className="px-5 py-2.5 bg-[#e58625] text-white rounded-[10px] font-serif font-medium text-[13px] hover:bg-[#d47920] transition-colors disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
{emailModalSubmitting ? 'Sending...' : 'Send Verification Link'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-20 12:37:22 +05:30
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|