// src/pages/admin/components/ProfileManager.tsx import React, { useState, useEffect } from 'react'; import type { Profile, Address, UserProfile } from '../../../types'; import { useApi } from '../../../hooks/useApi'; import * as apiClient from '../../../services/apiClient'; 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'; // This path is correct 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'; 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 } // --- API Hook Wrappers --- // These wrappers adapt the apiClient functions (which expect an ApiOptions object) // to the signature expected by the useApi hook (which passes a raw AbortSignal). // They are defined outside the component to ensure they have a stable identity // across re-renders, preventing infinite loops in useEffect hooks. const updateAddressWrapper = (data: Partial
, signal?: AbortSignal) => apiClient.updateUserAddress(data, { signal }); const updatePasswordWrapper = (password: string, signal?: AbortSignal) => apiClient.updateUserPassword(password, { signal }); const exportDataWrapper = (signal?: AbortSignal) => apiClient.exportUserData({ signal }); const deleteAccountWrapper = (password: string, signal?: AbortSignal) => apiClient.deleteUserAccount(password, { signal }); const updatePreferencesWrapper = (prefs: Partial, signal?: AbortSignal) => apiClient.updateUserPreferences(prefs, { signal }); const updateProfileWrapper = (data: Partial, signal?: AbortSignal) => apiClient.updateUserProfile(data, { signal }); 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); const { execute: updateProfile, loading: profileLoading } = useApi]>( updateProfileWrapper, ); const { execute: updateAddress, loading: addressLoading } = useApi]>( updateAddressWrapper, ); // Password state const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const { execute: updatePassword, loading: passwordLoading } = useApi( updatePasswordWrapper, ); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); // Data & Privacy state const { execute: exportData, loading: exportLoading } = useApi(exportDataWrapper); const { execute: deleteAccount, loading: deleteLoading } = useApi( deleteAccountWrapper, ); // Preferences state const { execute: updatePreferences } = useApi]>( updatePreferencesWrapper, ); 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. // Because useApi() catches errors and returns null, we can safely use Promise.all. const promisesToRun = []; if (profileDataChanged) { logger.debug('[handleProfileSave] Queuing profile update promise.'); promisesToRun.push(updateProfile({ full_name: fullName, avatar_url: avatarUrl })); } if (addressDataChanged) { logger.debug('[handleProfileSave] Queuing address update promise.'); promisesToRun.push(updateAddress(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' || (result.status === 'fulfilled' && !result.value)) { 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 useApi hook should have shown an error. The modal will remain open.', ); } } catch (error) { // This catch block is a safeguard. In normal operation, the useApi hook // should prevent any promises from rejecting. 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; } const result = await updatePassword(password); if (result) { notifySuccess('Password updated successfully!'); setPassword(''); setConfirmPassword(''); } }; const handleExportData = async () => { const userData = await exportData(); if (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 const result = await deleteAccount(passwordForDelete); if (result) { // useApi returns null on failure, so this check is sufficient. notifySuccess('Account deleted successfully. You will be logged out shortly.'); setTimeout(() => { onClose(); onSignOut(); }, 3000); } }; const handleToggleDarkMode = async (newMode: boolean) => { const updatedProfile = await updatePreferences({ darkMode: newMode }); if (updatedProfile) { onProfileUpdate(updatedProfile); } }; const handleToggleUnitSystem = async (newSystem: 'metric' | 'imperial') => { const updatedProfile = await updatePreferences({ unitSystem: newSystem }); if (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.

)}
)}
); };