// src/pages/admin/components/ProfileManager.tsx import React, { useState, useEffect } from 'react'; import type { Profile, Address, UserProfile } from '../../../types'; import { notifySuccess, notifyError } from '../../../services/notificationService'; import { logger } from '../../../services/logger.client'; import { LoadingSpinner } from '../../../components/LoadingSpinner'; import { XMarkIcon } from '../../../components/icons/XMarkIcon'; import { GoogleIcon } from '../../../components/icons/GoogleIcon'; import { GithubIcon } from '../../../components/icons/GithubIcon'; import { ConfirmationModal } from '../../../components/ConfirmationModal'; import { PasswordInput } from '../../../components/PasswordInput'; import { MapView } from '../../../components/MapView'; import type { AuthStatus } from '../../../hooks/useAuth'; import { AuthView } from './AuthView'; import { AddressForm } from './AddressForm'; import { useProfileAddress } from '../../../hooks/useProfileAddress'; import { useUpdateProfileMutation, useUpdateAddressMutation, useUpdatePasswordMutation, useUpdatePreferencesMutation, useExportDataMutation, useDeleteAccountMutation, } from '../../../hooks/mutations/useProfileMutations'; export interface ProfileManagerProps { isOpen: boolean; onClose: () => void; authStatus: AuthStatus; userProfile: UserProfile | null; // Can be null for login/register onProfileUpdate: (updatedProfile: Profile) => void; onSignOut: () => void; onLoginSuccess: (user: UserProfile, token: string, rememberMe: boolean) => void; // Add login handler } export const ProfileManager: React.FC = ({ isOpen, onClose, authStatus, userProfile, onProfileUpdate, onSignOut, onLoginSuccess, }) => { const [activeTab, setActiveTab] = useState('profile'); // Profile state const [fullName, setFullName] = useState(userProfile?.full_name || ''); const [avatarUrl, setAvatarUrl] = useState(userProfile?.avatar_url || ''); // Address logic is now encapsulated in this custom hook. const { address, initialAddress, isGeocoding, handleAddressChange, handleManualGeocode } = useProfileAddress(userProfile, isOpen); // TanStack Query mutations const updateProfileMutation = useUpdateProfileMutation(); const updateAddressMutation = useUpdateAddressMutation(); const updatePasswordMutation = useUpdatePasswordMutation(); const updatePreferencesMutation = useUpdatePreferencesMutation(); const exportDataMutation = useExportDataMutation(); const deleteAccountMutation = useDeleteAccountMutation(); const profileLoading = updateProfileMutation.isPending; const addressLoading = updateAddressMutation.isPending; const passwordLoading = updatePasswordMutation.isPending; const exportLoading = exportDataMutation.isPending; const deleteLoading = deleteAccountMutation.isPending; // Password state const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); const [passwordForDelete, setPasswordForDelete] = useState(''); useEffect(() => { // Only reset state when the modal is opened. // Do not reset on profile changes, which can happen during sign-out. logger.debug('[useEffect] Running effect due to change in isOpen or userProfile.', { isOpen, profileExists: !!userProfile, }); if (isOpen && userProfile) { // Ensure userProfile exists before setting state logger.debug('[useEffect] Modal is open with a valid profile. Resetting component state.'); setFullName(userProfile.full_name || ''); setAvatarUrl(userProfile.avatar_url || ''); setActiveTab('profile'); setIsConfirmingDelete(false); setPasswordForDelete(''); } }, [isOpen, userProfile]); // Depend on isOpen and userProfile const handleProfileSave = async (e: React.FormEvent) => { e.preventDefault(); logger.debug('[handleProfileSave] Save process started.'); if (!userProfile) { notifyError('Cannot save profile, no user is logged in.'); logger.warn('[handleProfileSave] Aborted: No user is logged in.'); return; } // Determine if profile or address data has changed const profileDataChanged = fullName !== userProfile?.full_name || avatarUrl !== userProfile?.avatar_url; const addressDataChanged = JSON.stringify(address) !== JSON.stringify(initialAddress); // --- Start Debug Logging --- logger.debug('[handleProfileSave] Checking for data changes.', { profileDataChanged, addressDataChanged, currentFullName: fullName, initialFullName: userProfile?.full_name, currentAvatarUrl: avatarUrl, initialAvatarUrl: userProfile?.avatar_url, currentAddress: JSON.stringify(address), initialAddress: JSON.stringify(initialAddress), }); // --- End Debug Logging --- if (!profileDataChanged && !addressDataChanged) { notifySuccess('No changes to save.'); logger.debug('[handleProfileSave] No changes detected. Closing modal.'); onClose(); return; } // Create an array of promises for the API calls that need to be made. const promisesToRun: Promise[] = []; if (profileDataChanged) { logger.debug('[handleProfileSave] Queuing profile update promise.'); promisesToRun.push( updateProfileMutation.mutateAsync({ full_name: fullName, avatar_url: avatarUrl }), ); } if (addressDataChanged) { logger.debug('[handleProfileSave] Queuing address update promise.'); promisesToRun.push(updateAddressMutation.mutateAsync(address)); } try { logger.debug(`[handleProfileSave] Awaiting ${promisesToRun.length} promises...`); // Use Promise.allSettled to handle partial successes gracefully. const results = await Promise.allSettled(promisesToRun); logger.debug('[handleProfileSave] Promise.allSettled finished.', { results }); let anyFailures = false; let successfulProfileUpdate: Profile | null = null; // Determine which promises succeeded or failed. results.forEach((result, index) => { const isProfilePromise = profileDataChanged && index === 0; if (result.status === 'rejected') { anyFailures = true; } else if (result.status === 'fulfilled' && isProfilePromise) { successfulProfileUpdate = result.value as Profile; } }); // If the profile update itself was successful, notify the parent component. if (successfulProfileUpdate) { onProfileUpdate(successfulProfileUpdate); } // Only show the global success message and close the modal if everything worked. if (!anyFailures) { notifySuccess('Profile updated successfully!'); onClose(); } else { logger.warn( '[handleProfileSave] One or more operations failed. The mutation hook should have shown an error. The modal will remain open.', ); } } catch (error) { // This catch block is a safeguard for unexpected errors. logger.error( { err: error }, "[CRITICAL] An unexpected error was caught directly in handleProfileSave's catch block.", ); notifyError( `An unexpected critical error occurred: ${error instanceof Error ? error.message : String(error)}`, ); } // This log confirms the function has completed its execution. logger.debug('[handleProfileSave] Save process finished.'); }; const handleOAuthLink = async (provider: 'google' | 'github') => { // This will redirect the user to the OAuth provider to link the account. // TODO: This is a placeholder. Implement OAuth account linking via the Passport.js backend. if (!userProfile) { return; // Should not be possible to see this button if not logged in } const errorMessage = `Account linking with ${provider} is not yet implemented.`; // This was a duplicate, fixed. logger.warn(errorMessage, { userId: userProfile.user.user_id }); notifyError(errorMessage); }; const handlePasswordUpdate = async (e: React.FormEvent) => { e.preventDefault(); if (password !== confirmPassword) { notifyError('Passwords do not match.'); return; } if (password.length < 6) { notifyError('Password must be at least 6 characters long.'); return; } updatePasswordMutation.mutate( { password }, { onSuccess: () => { notifySuccess('Password updated successfully!'); setPassword(''); setConfirmPassword(''); }, }, ); }; const handleExportData = async () => { exportDataMutation.mutate(undefined, { onSuccess: (userData) => { const jsonString = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(userData, null, 2))}`; const link = document.createElement('a'); link.href = jsonString; link.download = `flyer-crawler-data-export-${new Date().toISOString().split('T')[0]}.json`; link.click(); }, }); }; const handleDeleteAccount = async () => { setIsDeleteModalOpen(false); // Close the confirmation modal deleteAccountMutation.mutate( { password: passwordForDelete }, { onSuccess: () => { notifySuccess('Account deleted successfully. You will be logged out shortly.'); setTimeout(() => { onClose(); onSignOut(); }, 3000); }, }, ); }; const handleToggleDarkMode = async (newMode: boolean) => { updatePreferencesMutation.mutate( { darkMode: newMode }, { onSuccess: (updatedProfile) => { onProfileUpdate(updatedProfile); }, }, ); }; const handleToggleUnitSystem = async (newSystem: 'metric' | 'imperial') => { updatePreferencesMutation.mutate( { unitSystem: newSystem }, { onSuccess: (updatedProfile) => { onProfileUpdate(updatedProfile); }, }, ); }; if (!isOpen) return null; return (
setIsDeleteModalOpen(false)} onConfirm={handleDeleteAccount} // Pass the handler directly title="Confirm Account Deletion" message={ <> Are you absolutely sure you want to delete your account? This action is permanent and cannot be undone. } confirmButtonText="Yes, Delete My Account" />
e.stopPropagation()} > {authStatus === 'SIGNED_OUT' ? ( ) : ( // Wrap the authenticated view in a React Fragment to return a single element <>

My Account

Manage your profile, preferences, and security.

{activeTab === 'profile' && (
setFullName(e.target.value)} className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm" />
setAvatarUrl(e.target.value)} className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm" />
{address.latitude && address.longitude && (
)}
)} {activeTab === 'security' && (
setPassword(e.target.value)} placeholder="••••••••" required className="mt-1" showStrength />
setConfirmPassword(e.target.value)} placeholder="••••••••" required className="mt-1" />

Link Social Accounts

Connect other accounts for easier sign-in.

)} {activeTab === 'data' && (

Export Your Data

Download a JSON file of your profile, watched items, and shopping lists.

Danger Zone

This action is permanent and cannot be undone. All your data will be erased.

{!isConfirmingDelete ? ( ) : (
{ e.preventDefault(); setIsDeleteModalOpen(true); }} className="mt-4 space-y-3 bg-white dark:bg-gray-800 p-4 rounded-md border border-red-500/50" >

To confirm, please enter your current password.

setPasswordForDelete(e.target.value)} required placeholder="Enter your password" />
)}
)} {activeTab === 'preferences' && (

Theme

Choose your preferred visual theme.

Unit System

Select your preferred system of measurement.

)}
)}
); };