moar fixes + unit test review of routes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 57m53s

This commit is contained in:
2025-12-19 14:20:22 -08:00
parent aa437d6139
commit e62739810e
38 changed files with 1167 additions and 288 deletions

View File

@@ -12,11 +12,11 @@ import { GoogleIcon } from '../../../components/icons/GoogleIcon';
import { GithubIcon } from '../../../components/icons/GithubIcon';
import { ConfirmationModal } from '../../../components/ConfirmationModal';
import { PasswordInput } from './PasswordInput';
import { AddressForm } from './AddressForm';
import { MapView } from '../../../components/MapView';
import { useDebounce } from '../../../hooks/useDebounce';
import type { AuthStatus } from '../../../hooks/useAuth';
import { AuthView } from './AuthView';
import { AddressForm } from './AddressForm';
import { useProfileAddress } from '../../../hooks/useProfileAddress';
interface ProfileManagerProps {
isOpen: boolean;
@@ -34,14 +34,12 @@ interface ProfileManagerProps {
// 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 updateProfileWrapper = (data: Partial<Profile>, signal?: AbortSignal) => apiClient.updateUserProfile(data, { signal });
const updateAddressWrapper = (data: Partial<Address>, signal?: AbortSignal) => apiClient.updateUserAddress(data, { signal });
const geocodeWrapper = (address: string, signal?: AbortSignal) => apiClient.geocodeAddress(address, { 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<Profile['preferences']>, signal?: AbortSignal) => apiClient.updateUserPreferences(prefs, { signal });
const fetchAddressWrapper = (id: number, signal?: AbortSignal) => apiClient.getUserAddress(id, { signal });
const updateProfileWrapper = (data: Partial<Profile>, signal?: AbortSignal) => apiClient.updateUserProfile(data, { signal });
export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose, user, authStatus, profile, onProfileUpdate, onSignOut, onLoginSuccess }) => { // This line had a type error due to syntax issues below.
const [activeTab, setActiveTab] = useState('profile');
@@ -49,13 +47,12 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
// Profile state
const [fullName, setFullName] = useState(profile?.full_name || '');
const [avatarUrl, setAvatarUrl] = useState(profile?.avatar_url || '');
const [address, setAddress] = useState<Partial<Address>>({});
const [initialAddress, setInitialAddress] = useState<Partial<Address>>({}); // Store initial address for comparison
// Address logic is now encapsulated in this custom hook.
const { address, initialAddress, isGeocoding, handleAddressChange, handleManualGeocode } = useProfileAddress(profile, isOpen);
const { execute: updateProfile, loading: profileLoading } = useApi<Profile, [Partial<Profile>]>(updateProfileWrapper);
const { execute: updateAddress, loading: addressLoading } = useApi<Address, [Partial<Address>]>(updateAddressWrapper);
const { execute: geocode, loading: isGeocoding } = useApi<{ lat: number; lng: number }, [string]>(geocodeWrapper);
// Password state
const [password, setPassword] = useState('');
@@ -73,48 +70,19 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);
const [passwordForDelete, setPasswordForDelete] = useState('');
// New hook to fetch address details
const { execute: fetchAddress } = useApi<Address, [number]>(fetchAddressWrapper);
const handleAddressFetch = useCallback(async (addressId: number) => {
logger.debug(`[handleAddressFetch] Starting fetch for addressId: ${addressId}`);
const fetchedAddress = await fetchAddress(addressId);
if (fetchedAddress) {
logger.debug('[handleAddressFetch] Successfully fetched address:', fetchedAddress);
setAddress(fetchedAddress);
setInitialAddress(fetchedAddress); // Set initial address on fetch
} else {
logger.warn(`[handleAddressFetch] Fetch returned null or undefined for addressId: ${addressId}. This might indicate a network error caught by useApi.`);
}
}, [fetchAddress]);
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 profile.', { isOpen, profileExists: !!profile });
if (isOpen && profile) { // Ensure profile exists before setting state
logger.debug('[useEffect] Modal is open with a valid profile. Resetting component state.');
setFullName(profile?.full_name || '');
setAvatarUrl(profile?.avatar_url || '');
// If the user has an address, fetch its details
if (profile.address_id) {
logger.debug(`[useEffect] Profile has address_id: ${profile.address_id}. Calling handleAddressFetch.`);
handleAddressFetch(profile.address_id);
} else {
// Reset address form if user has no address
logger.debug('[useEffect] Profile has no address_id. Resetting address form.');
setAddress({});
setInitialAddress({});
}
setFullName(profile.full_name || '');
setAvatarUrl(profile.avatar_url || '');
setActiveTab('profile');
setIsConfirmingDelete(false);
setPasswordForDelete('');
} else {
logger.debug('[useEffect] Modal is closed or profile is null. Resetting address state only.');
setAddress({});
setInitialAddress({});
}
}, [isOpen, profile, handleAddressFetch]); // Depend on isOpen and profile
}, [isOpen, profile]); // Depend on isOpen and profile
const handleProfileSave = async (e: React.FormEvent) => {
e.preventDefault();
@@ -203,75 +171,6 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
// This log confirms the function has completed its execution.
logger.debug('[handleProfileSave] Save process finished.');
};
// --- DEBUG LOGGING ---
// Log the loading states on every render to debug the submit button's disabled state.
logger.debug('[ComponentRender] Loading states:', { profileLoading, addressLoading });
// Log the function reference itself to see if it's being recreated unexpectedly.
// We convert it to a string to see a snapshot in time.
logger.debug(`[ComponentRender] handleProfileSave function created.`);
const handleAddressChange = (field: keyof Address, value: string) => {
setAddress(prev => ({ ...prev, [field]: value }));
};
const handleManualGeocode = async () => {
const addressString = [
address.address_line_1,
address.city,
address.province_state,
address.postal_code,
address.country,
].filter(Boolean).join(', ');
if (!addressString) {
toast.error('Please fill in the address fields before geocoding.');
return;
}
const result = await geocode(addressString);
if (result) {
const { lat, lng } = result;
setAddress(prev => ({ ...prev, latitude: lat, longitude: lng }));
toast.success('Address re-geocoded successfully!');
}
};
// --- Automatic Geocoding Logic ---
const debouncedAddress = useDebounce(address, 1500); // Debounce address state by 1.5 seconds
useEffect(() => {
// This effect runs when the debouncedAddress value changes.
const handleGeocode = async () => {
logger.debug('[handleGeocode] Effect triggered by debouncedAddress change');
// Only trigger if the core address fields are present and have changed.
const addressString = [
debouncedAddress.address_line_1,
debouncedAddress.city,
debouncedAddress.province_state,
debouncedAddress.postal_code,
debouncedAddress.country,
].filter(Boolean).join(', ');
logger.debug(`[handleGeocode] addressString generated: "${addressString}"`);
// Don't geocode an empty address or if we already have coordinates for this exact address.
if (!addressString || (debouncedAddress.latitude && debouncedAddress.longitude)) {
logger.debug('[handleGeocode] Skipping geocode: empty string or coordinates already exist');
return;
}
logger.debug('[handleGeocode] Calling geocode API...');
const result = await geocode(addressString);
if (result) {
logger.debug('[handleGeocode] API returned result:', result);
const { lat, lng } = result;
setAddress(prev => ({ ...prev, latitude: lat, longitude: lng }));
toast.success('Address geocoded successfully!');
}
};
handleGeocode();
}, [debouncedAddress]); // Dependency array ensures this runs only when the debounced value changes.
const handleOAuthLink = async (provider: 'google' | 'github') => {
// This will redirect the user to the OAuth provider to link the account.