This commit is contained in:
Iliyan Angelov
2025-11-21 01:20:51 +02:00
parent a38ab4fa82
commit 6f85b8cf17
242 changed files with 7154 additions and 14492 deletions

View File

@@ -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]" />