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,
};
};
|