updates
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
||||
Mail,
|
||||
Phone,
|
||||
Save,
|
||||
Loader2,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Lock,
|
||||
@@ -29,7 +28,6 @@ import { useAsync } from '../../hooks/useAsync';
|
||||
import { useGlobalLoading } from '../../contexts/GlobalLoadingContext';
|
||||
import { normalizeImageUrl } from '../../utils/imageUtils';
|
||||
|
||||
// Validation schema
|
||||
const profileValidationSchema = yup.object().shape({
|
||||
name: yup
|
||||
.string()
|
||||
@@ -81,7 +79,7 @@ const ProfilePage: React.FC = () => {
|
||||
confirm: false,
|
||||
});
|
||||
|
||||
// MFA state
|
||||
|
||||
const [mfaStatus, setMfaStatus] = useState<{mfa_enabled: boolean; backup_codes_count: number} | null>(null);
|
||||
const [mfaSecret, setMfaSecret] = useState<string | null>(null);
|
||||
const [mfaQrCode, setMfaQrCode] = useState<string | null>(null);
|
||||
@@ -89,13 +87,13 @@ const ProfilePage: React.FC = () => {
|
||||
const [showBackupCodes, setShowBackupCodes] = useState<string[] | null>(null);
|
||||
const [showMfaSecret, setShowMfaSecret] = useState<boolean>(false);
|
||||
|
||||
// Fetch profile data
|
||||
|
||||
const fetchProfile = async () => {
|
||||
const response = await authService.getProfile();
|
||||
if (response.status === 'success' || response.success) {
|
||||
const user = response.data?.user || response.data;
|
||||
if (user) {
|
||||
setUser(user);
|
||||
setUser(user as any);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -114,7 +112,7 @@ const ProfilePage: React.FC = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Profile form
|
||||
|
||||
const {
|
||||
register: registerProfile,
|
||||
handleSubmit: handleSubmitProfile,
|
||||
@@ -123,13 +121,13 @@ const ProfilePage: React.FC = () => {
|
||||
} = useForm<ProfileFormData>({
|
||||
resolver: yupResolver(profileValidationSchema),
|
||||
defaultValues: {
|
||||
name: userInfo?.name || '',
|
||||
email: userInfo?.email || '',
|
||||
phone: userInfo?.phone || '',
|
||||
name: ((userInfo && 'user' in userInfo ? userInfo.user : userInfo) as any)?.name || '',
|
||||
email: ((userInfo && 'user' in userInfo ? userInfo.user : userInfo) as any)?.email || '',
|
||||
phone: ((userInfo && 'user' in userInfo ? userInfo.user : userInfo) as any)?.phone || '',
|
||||
},
|
||||
});
|
||||
|
||||
// Password form
|
||||
|
||||
const {
|
||||
register: registerPassword,
|
||||
handleSubmit: handleSubmitPassword,
|
||||
@@ -139,11 +137,11 @@ const ProfilePage: React.FC = () => {
|
||||
resolver: yupResolver(passwordValidationSchema),
|
||||
});
|
||||
|
||||
// Fetch MFA status
|
||||
|
||||
const fetchMFAStatus = async () => {
|
||||
try {
|
||||
const response = await authService.getMFAStatus();
|
||||
// Response is now directly the status data, not wrapped in data
|
||||
|
||||
if (response) {
|
||||
setMfaStatus({
|
||||
mfa_enabled: response.mfa_enabled || false,
|
||||
@@ -155,33 +153,33 @@ const ProfilePage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Update form when profile data loads
|
||||
|
||||
useEffect(() => {
|
||||
if (profileData || userInfo) {
|
||||
const data = profileData || userInfo;
|
||||
const data = profileData || (userInfo && 'user' in userInfo ? userInfo.user : userInfo);
|
||||
resetProfile({
|
||||
name: data?.name || '',
|
||||
email: data?.email || '',
|
||||
phone: data?.phone || '',
|
||||
name: (data as any)?.name || '',
|
||||
email: (data as any)?.email || '',
|
||||
phone: (data as any)?.phone || '',
|
||||
});
|
||||
if (data?.avatar) {
|
||||
// Normalize avatar URL when loading
|
||||
setAvatarPreview(normalizeImageUrl(data.avatar));
|
||||
if ((data as any)?.avatar) {
|
||||
|
||||
setAvatarPreview(normalizeImageUrl((data as any).avatar));
|
||||
} else {
|
||||
setAvatarPreview(null);
|
||||
}
|
||||
}
|
||||
}, [profileData, userInfo, resetProfile]);
|
||||
|
||||
// Fetch MFA status when MFA tab is active
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'mfa') {
|
||||
fetchMFAStatus();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [activeTab]);
|
||||
|
||||
// Handle profile update
|
||||
|
||||
const onSubmitProfile = async (data: ProfileFormData) => {
|
||||
try {
|
||||
setLoading(true, 'Updating profile...');
|
||||
@@ -203,21 +201,22 @@ const ProfilePage: React.FC = () => {
|
||||
}
|
||||
} else {
|
||||
const { updateUser } = await import('../../services/api/userService');
|
||||
const response = await updateUser(userInfo!.id, {
|
||||
const user = userInfo && 'user' in userInfo ? userInfo.user : userInfo;
|
||||
const response = await updateUser((user as any)?.id || (userInfo as any)?.id, {
|
||||
full_name: data.name,
|
||||
email: data.email,
|
||||
phone_number: data.phone,
|
||||
});
|
||||
|
||||
if (response.success || response.status === 'success') {
|
||||
if ((response as any).success || (response as any).status === 'success') {
|
||||
const updatedUser = response.data?.user || response.data;
|
||||
if (updatedUser) {
|
||||
setUser({
|
||||
id: updatedUser.id,
|
||||
name: updatedUser.full_name || updatedUser.name,
|
||||
name: (updatedUser as any).full_name || (updatedUser as any).name,
|
||||
email: updatedUser.email,
|
||||
phone: updatedUser.phone_number || updatedUser.phone,
|
||||
avatar: updatedUser.avatar,
|
||||
phone: (updatedUser as any).phone_number || (updatedUser as any).phone,
|
||||
avatar: (updatedUser as any).avatar,
|
||||
role: updatedUser.role,
|
||||
});
|
||||
toast.success('Profile updated successfully!');
|
||||
@@ -236,7 +235,7 @@ const ProfilePage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle password change
|
||||
|
||||
const onSubmitPassword = async (data: PasswordFormData) => {
|
||||
try {
|
||||
setLoading(true, 'Changing password...');
|
||||
@@ -253,11 +252,12 @@ const ProfilePage: React.FC = () => {
|
||||
}
|
||||
} else {
|
||||
const { updateUser } = await import('../../services/api/userService');
|
||||
const response = await updateUser(userInfo!.id, {
|
||||
const user = userInfo && 'user' in userInfo ? userInfo.user : userInfo;
|
||||
const response = await updateUser((user as any)?.id || (userInfo as any)?.id, {
|
||||
password: data.newPassword,
|
||||
});
|
||||
|
||||
if (response.success || response.status === 'success') {
|
||||
if ((response as any).success || (response as any).status === 'success') {
|
||||
toast.success('Password changed successfully!');
|
||||
resetPassword();
|
||||
}
|
||||
@@ -273,44 +273,44 @@ const ProfilePage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle avatar upload
|
||||
|
||||
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 (max 2MB)
|
||||
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
toast.error('Image size must be less than 2MB');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create preview
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setAvatarPreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// Upload to backend
|
||||
|
||||
try {
|
||||
setLoading(true, 'Uploading avatar...');
|
||||
const response = await authService.uploadAvatar(file);
|
||||
|
||||
if (response.status === 'success' && response.data?.user) {
|
||||
const updatedUser = response.data.user;
|
||||
// Use full_url if available, otherwise normalize the avatar URL
|
||||
const avatarUrl = response.data.full_url || normalizeImageUrl(updatedUser.avatar);
|
||||
|
||||
const avatarUrl = (response.data as any).full_url || normalizeImageUrl((updatedUser as any).avatar);
|
||||
setUser({
|
||||
id: updatedUser.id,
|
||||
name: updatedUser.name,
|
||||
name: (updatedUser as any).name || (updatedUser as any).full_name,
|
||||
email: updatedUser.email,
|
||||
phone: updatedUser.phone,
|
||||
phone: (updatedUser as any).phone || (updatedUser as any).phone_number,
|
||||
avatar: avatarUrl,
|
||||
role: updatedUser.role,
|
||||
});
|
||||
@@ -324,11 +324,11 @@ const ProfilePage: React.FC = () => {
|
||||
error.message ||
|
||||
'Failed to upload avatar';
|
||||
toast.error(errorMessage);
|
||||
// Reset preview on error
|
||||
|
||||
setAvatarPreview(userInfo?.avatar ? normalizeImageUrl(userInfo.avatar) : null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// Reset file input
|
||||
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
@@ -355,7 +355,7 @@ const ProfilePage: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-gray-100 to-gray-50 py-4 sm:py-6 lg:py-8 px-3 sm:px-4 lg:px-8">
|
||||
<div className="container mx-auto max-w-5xl">
|
||||
{/* Header */}
|
||||
{}
|
||||
<div className="mb-6 sm:mb-8 animate-fade-in">
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-serif font-bold text-gray-900 tracking-tight mb-2">
|
||||
Profile Settings
|
||||
@@ -365,7 +365,7 @@ const ProfilePage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
{}
|
||||
<div className="mb-4 sm:mb-6 border-b border-gray-200 overflow-x-auto">
|
||||
<div className="flex space-x-4 sm:space-x-8 min-w-max">
|
||||
<button
|
||||
@@ -404,11 +404,11 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Tab */}
|
||||
{}
|
||||
{activeTab === 'profile' && (
|
||||
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl animate-slide-up">
|
||||
<form onSubmit={handleSubmitProfile(onSubmitProfile)} className="space-y-5 sm:space-y-6">
|
||||
{/* Avatar Section */}
|
||||
{}
|
||||
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6 pb-5 sm:pb-6 border-b border-gray-200">
|
||||
<div className="relative">
|
||||
{avatarPreview || userInfo?.avatar ? (
|
||||
@@ -431,23 +431,13 @@ const ProfilePage: React.FC = () => {
|
||||
id="avatar-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleAvatarChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="text-center sm:text-left flex-1">
|
||||
<h3 className="text-lg sm:text-xl font-semibold text-gray-900 mb-1">
|
||||
{userInfo?.name || 'User'}
|
||||
</h3>
|
||||
<p className="text-sm sm:text-base text-gray-500 mb-1">{userInfo?.email}</p>
|
||||
<p className="text-xs sm:text-sm text-[#d4af37] font-medium tracking-wide uppercase">
|
||||
{userInfo?.role?.charAt(0).toUpperCase() + userInfo?.role?.slice(1)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Full Name */}
|
||||
<div>
|
||||
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
|
||||
<User className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
|
||||
@@ -470,7 +460,7 @@ const ProfilePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
{}
|
||||
<div>
|
||||
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
|
||||
<Mail className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
|
||||
@@ -493,7 +483,7 @@ const ProfilePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Phone */}
|
||||
{}
|
||||
<div>
|
||||
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
|
||||
<Phone className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
|
||||
@@ -516,7 +506,7 @@ const ProfilePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
{}
|
||||
<div className="flex justify-end pt-4 sm:pt-5 border-t border-gray-200">
|
||||
<button
|
||||
type="submit"
|
||||
@@ -531,11 +521,11 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Password Tab */}
|
||||
{}
|
||||
{activeTab === 'password' && (
|
||||
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl animate-slide-up">
|
||||
<form onSubmit={handleSubmitPassword(onSubmitPassword)} className="space-y-5 sm:space-y-6">
|
||||
{/* Info Banner */}
|
||||
{}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200/60 rounded-sm p-4 sm:p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-1.5 sm:p-2 bg-blue-100 rounded-full flex-shrink-0">
|
||||
@@ -559,7 +549,7 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Password */}
|
||||
{}
|
||||
<div>
|
||||
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
|
||||
<Lock className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
|
||||
@@ -595,7 +585,7 @@ const ProfilePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* New Password */}
|
||||
{}
|
||||
<div>
|
||||
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
|
||||
<Lock className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
|
||||
@@ -631,7 +621,7 @@ const ProfilePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirm Password */}
|
||||
{}
|
||||
<div>
|
||||
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
|
||||
<Lock className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
|
||||
@@ -667,7 +657,7 @@ const ProfilePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
{}
|
||||
<div className="flex justify-end pt-4 sm:pt-5 border-t border-gray-200">
|
||||
<button
|
||||
type="submit"
|
||||
@@ -682,10 +672,10 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MFA Tab */}
|
||||
{}
|
||||
{activeTab === 'mfa' && (
|
||||
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl animate-slide-up space-y-5 sm:space-y-6">
|
||||
{/* Header */}
|
||||
{}
|
||||
<div>
|
||||
<h2 className="text-xl sm:text-2xl font-serif font-semibold text-gray-900 mb-2 flex items-center gap-2">
|
||||
<Shield className="w-5 h-5 sm:w-6 sm:h-6 text-[#d4af37]" />
|
||||
@@ -697,9 +687,9 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{mfaStatus?.mfa_enabled ? (
|
||||
/* MFA Enabled State */
|
||||
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
{/* Status Card */}
|
||||
{}
|
||||
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200/60 rounded-sm p-4 sm:p-5 shadow-lg">
|
||||
<div className="flex items-start sm:items-center gap-3 sm:gap-4">
|
||||
<div className="p-2 sm:p-2.5 bg-green-100 rounded-full flex-shrink-0">
|
||||
@@ -717,7 +707,7 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backup Codes */}
|
||||
{}
|
||||
{showBackupCodes && (
|
||||
<div className="bg-gradient-to-r from-yellow-50 to-amber-50 border border-yellow-200/60 rounded-sm p-4 sm:p-5 shadow-lg">
|
||||
<div className="mb-4">
|
||||
@@ -757,7 +747,7 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Regenerate Backup Codes */}
|
||||
{}
|
||||
<div className="bg-white border border-gray-200 rounded-sm p-4 sm:p-5">
|
||||
<h3 className="text-sm sm:text-base font-semibold text-gray-900 mb-2 flex items-center gap-2">
|
||||
<RefreshCw className="w-4 h-4 sm:w-5 sm:h-5 text-[#d4af37]" />
|
||||
@@ -789,7 +779,7 @@ const ProfilePage: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Disable MFA */}
|
||||
{}
|
||||
<div className="border-t border-gray-200 pt-5 sm:pt-6">
|
||||
<h3 className="text-sm sm:text-base font-semibold text-red-900 mb-2 flex items-center gap-2">
|
||||
<ShieldOff className="w-4 h-4 sm:w-5 sm:h-5 text-red-600" />
|
||||
@@ -824,10 +814,10 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* MFA Setup Flow */
|
||||
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
{!mfaSecret ? (
|
||||
/* Step 1: Initialize MFA */
|
||||
|
||||
<div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
@@ -852,9 +842,9 @@ const ProfilePage: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
/* Step 2: Verify and Enable */
|
||||
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
{/* QR Code Section */}
|
||||
{}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200/60 rounded-sm p-4 sm:p-5 shadow-lg">
|
||||
<h3 className="font-semibold text-blue-900 mb-2 text-sm sm:text-base flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
@@ -903,7 +893,7 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verification Section */}
|
||||
{}
|
||||
<div className="bg-white border border-gray-200 rounded-sm p-4 sm:p-5">
|
||||
<h3 className="font-semibold text-gray-900 mb-2 text-sm sm:text-base flex items-center gap-2">
|
||||
<KeyRound className="w-4 h-4 sm:w-5 sm:h-5 text-[#d4af37]" />
|
||||
|
||||
Reference in New Issue
Block a user