All files / src/features/setting/hooks use-profile-settings.ts

93.75% Statements 75/80
83.67% Branches 41/49
87.5% Functions 14/16
93.75% Lines 75/80

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272                                          2x 69x   69x     69x 69x     69x     69x         69x 19x 6x 6x     6x         69x 30x           69x                     69x     69x 19x   6x   6x                 6x               6x 1x           69x 19x 1x       69x 5x       69x   24x   24x 2x         69x   5x 5x 5x     5x 5x     5x   2x 2x         5x             5x   5x     4x 2x   2x   1x 1x 1x   1x   1x 1x 1x       1x 1x           1x   1x   1x 1x   1x       2x     2x 2x 2x       2x       5x 1x         3x     3x 1x             1x       69x     69x                                                         69x                                    
import { useEffect, useState, useOptimistic, startTransition } from 'react';
import { useSettingForm } from './use-setting-form';
import type { ProfileFormData } from '../schemas/profile.schema';
import { profileSchema } from '../schemas/profile.schema';
import { profileService } from '../services/profile.service';
import { useUserProfile } from '@/hooks/use-user-profile';
import { useUserStore } from '@/store/user-store';
import { formatAvatarUrl } from '@/lib/format-avatar';
 
interface ProfileData {
  name: string;
  email: string;
  phone: string;
  nationality: string;
  avatar: string | null;
}
 
interface UseProfileSettingsOptions {
  onProfileDataLoaded?: (profileData: ProfileData) => void;
}
 
export const useProfileSettings = (options?: UseProfileSettingsOptions) => {
  const { onProfileDataLoaded } = options || {};
 
  const [newAvatarFile, setNewAvatarFile] = useState<File | null>(null);
 
  // Use the centralized profile hook instead of loading profile data ourselves
  const { profile, isLoading, error: profileError, refetch } = useUserProfile();
  const { updateProfile } = useUserStore();
 
  // Optimistic updates for immediate UI feedback
  const [optimisticProfileName, setOptimisticProfileName] = useOptimistic(
    profile?.name || (profile as any)?.username || ''
  );
  const [optimisticAvatarUrl, setOptimisticAvatarUrl] = useOptimistic<
    string | null
  >(formatAvatarUrl(profile?.avatar) || null);
 
  // Update optimistic state when profile data changes from the server
  useEffect(() => {
    if (profile) {
      startTransition(() => {
        setOptimisticProfileName(
          profile.name || (profile as any).username || ''
        );
        setOptimisticAvatarUrl(formatAvatarUrl(profile.avatar) || null);
      });
    }
  }, [profile, setOptimisticProfileName, setOptimisticAvatarUrl]);
 
  useEffect(() => {
    updateProfile({
      name: optimisticProfileName,
      avatar: optimisticAvatarUrl,
    });
  }, [optimisticProfileName, optimisticAvatarUrl, updateProfile]);
 
  const settingForm = useSettingForm<ProfileFormData>(profileSchema, {
    successMessage: 'Profile updated successfully!',
    defaultValues: {
      name: '',
      email: '',
      phone: '',
      nationality: '',
    },
  });
 
  // Destructure methods from settingForm to avoid including the entire object in dependencies
  const { reset, setError } = settingForm;
 
  // Load and sync profile data when profile is available
  useEffect(() => {
    if (profile) {
      // Use the formatAvatarUrl utility function to handle avatar URL formatting
      const avatarUrlValue = formatAvatarUrl(profile.avatar);
 
      const loadedProfileData: ProfileData = {
        name: profile.name || (profile as any).username || '',
        email: profile.email || '',
        phone: profile.phone || '',
        nationality: profile.nationality || '',
        avatar: avatarUrlValue,
      };
 
      // Update form with loaded data
      reset({
        name: loadedProfileData.name,
        email: loadedProfileData.email,
        phone: loadedProfileData.phone,
        nationality: loadedProfileData.nationality,
      });
 
      // Notify callback about loaded profile data if provided
      if (onProfileDataLoaded) {
        onProfileDataLoaded(loadedProfileData);
      }
    }
  }, [profile, onProfileDataLoaded, reset]);
 
  // Set profile error if there's one
  useEffect(() => {
    if (profileError) {
      setError(profileError.message);
    }
  }, [profileError, setError]);
 
  const handleAvatarChange = (file: File | null) => {
    setNewAvatarFile(file);
  };
 
  // Clean up object URLs when component unmounts or when URLs change
  useEffect(() => {
    // This function will be called when optimisticAvatarUrl changes or component unmounts
    return () => {
      // Check if the optimisticAvatarUrl is an object URL (starts with blob:)
      if (optimisticAvatarUrl && optimisticAvatarUrl.startsWith('blob:')) {
        URL.revokeObjectURL(optimisticAvatarUrl);
      }
    };
  }, [optimisticAvatarUrl]);
 
  const submitProfile = async (data: ProfileFormData): Promise<void> => {
    // Store the original values before optimistic update for potential reversion
    const originalName = optimisticProfileName;
    const originalAvatarUrl = optimisticAvatarUrl;
    let tempUrl: string | null = null;
 
    // Apply optimistic updates immediately
    startTransition(() => {
      setOptimisticProfileName(data.name);
 
      // Apply optimistic avatar update if there's a new file
      if (newAvatarFile) {
        // Create a temporary object URL for immediate feedback
        tempUrl = URL.createObjectURL(newAvatarFile);
        setOptimisticAvatarUrl(tempUrl);
      }
    });
 
    // Prepare update data (excluding email as it cannot be changed)
    const updateData = {
      username: data.name,
      name: data.name,
      phone: data.phone,
      nationality: data.nationality,
    };
 
    try {
      // Update profile via API
      await profileService.updateProfile(updateData);
 
      // Handle avatar upload if there's a new file
      if (newAvatarFile) {
        try {
          const avatarResponse =
            await profileService.uploadAvatar(newAvatarFile);
 
          Eif (avatarResponse.avatar) {
            const avatar = avatarResponse.avatar;
            const newAvatarUrl = formatAvatarUrl(avatar);
 
            Eif (newAvatarUrl) {
              // If we previously created a temp URL, revoke it to prevent memory leaks
              Eif (tempUrl) {
                URL.revokeObjectURL(tempUrl);
                tempUrl = null; // Prevent double revocation in finally block
              }
 
              // Update the optimistic state with the actual URL from the server
              startTransition(() => {
                setOptimisticAvatarUrl(newAvatarUrl);
              });
            }
          }
 
          // Clear the new avatar file after successful upload
          setNewAvatarFile(null);
        } catch (avatarError) {
          console.error('Avatar upload failed:', avatarError);
          // Revert to original avatar on failure
          startTransition(() => {
            setOptimisticAvatarUrl(originalAvatarUrl);
          });
          throw avatarError; // Re-throw for the outer catch block
        }
      }
    } catch (error: any) {
      console.error('Profile update failed:', error);
 
      // Revert optimistic updates on failure
      startTransition(() => {
        setOptimisticProfileName(originalName);
        setOptimisticAvatarUrl(originalAvatarUrl);
      });
 
      // Re-throw the error to trigger form error handling
      throw error;
    } finally {
      // This cleanup function runs regardless of success or failure
      // It will properly clean up our temp URL if needed
      if (tempUrl) {
        URL.revokeObjectURL(tempUrl);
      }
    }
 
    // Refetch profile data to get the latest changes from server
    await refetch();
 
    // Notify callback about updated profile data if provided
    if (onProfileDataLoaded) {
      const updatedData: ProfileData = {
        name: data.name,
        email: data.email,
        phone: data.phone,
        nationality: data.nationality,
        avatar: optimisticAvatarUrl,
      };
      onProfileDataLoaded(updatedData);
    }
  };
 
  const handleSubmit = settingForm.handleFormSubmit(submitProfile);
 
  // Server action for form submission with FormData
  const profileAction = async (prevState: any, formData: FormData) => {
    try {
      // Create a synthetic event object for handleSubmit
      const event = {
        preventDefault: () => {},
        currentTarget: {
          elements: {
            name: { value: formData.get('name') },
            email: { value: formData.get('email') },
            phone: { value: formData.get('phone') },
            nationality: { value: formData.get('nationality') },
          },
        },
      };
 
      await handleSubmit(event as any);
 
      return {
        success: true,
        error: null,
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'An error occurred',
      };
    }
  };
 
  return {
    // Form state and methods
    ...settingForm,
 
    // Profile-specific state
    avatarUrl: optimisticAvatarUrl,
    newAvatarFile,
    isLoading,
 
    // Profile-specific actions
    handleAvatarChange,
    handleSubmit,
    profileAction,
 
    // Computed values
    isSubmitting: settingForm.formState.isSubmitting,
  };
};