All files / components ApiErrorAlert.tsx

41.66% Statements 5/12
57.14% Branches 12/21
100% Functions 2/2
41.66% Lines 5/12

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                                                            27x                                                               542x 515x       27x 27x                                                                                            
// src/components/ApiErrorAlert.tsx
import {type ReactElement} from 'react';
import {Alert, AlertTitle} from '@mui/material';
import type {SerializedError} from '@reduxjs/toolkit';
import type {FetchBaseQueryError} from '@reduxjs/toolkit/query';
 
/**
 * Standard API error response format from backend
 */
interface ApiErrorData {
	success: false;
	error: string; // Error type: "Authentication Error", "Validation Error", etc.
	message: string; // Specific message: "Invalid credentials", "Email already exists", etc.
}
 
interface ApiErrorAlertProps {
	/**
	 * Error from RTK Query mutation/query
	 */
	error: FetchBaseQueryError | SerializedError | undefined;
	/**
	 * Optional custom fallback message when error format is unexpected
	 */
	fallbackMessage?: string;
}
 
/**
 * Type guard to check if error data matches our API error format
 */
function isApiErrorData(data: unknown): data is ApiErrorData {
	return (
		typeof data === 'object' &&
		data !== null &&
		'error' in data &&
		'message' in data &&
		typeof (data as ApiErrorData).error === 'string' &&
		typeof (data as ApiErrorData).message === 'string'
	);
}
 
/**
 * Reusable error alert component for displaying API errors consistently.
 *
 * Handles the standard backend error format:
 * { success: false, error: "Error Type", message: "Specific details" }
 *
 * @example
 * ```tsx
 * const [login, { error }] = useLoginMutation();
 *
 * return (
 *   <>
 *     <ApiErrorAlert error={error} />
 *     {// rest of form}
 *   </>
 * );
 * ```
 */
export default function ApiErrorAlert({
										  error,
										  fallbackMessage = 'An unexpected error occurred. Please try again.'
									  }: ApiErrorAlertProps): ReactElement | null {
	if (error === undefined) {
		return null;
	}
 
	// Handle FetchBaseQueryError with our standard API format
	Eif ('data' in error && isApiErrorData(error.data)) {
		return (
			<Alert severity="error" sx={{width: '100%', mb: 2}}>
				<AlertTitle>{error.data.error}</AlertTitle>
				{error.data.message}
			</Alert>
		);
	}
 
	// Handle FetchBaseQueryError with status but no standard format
	if ('status' in error) {
		// Check for network errors
		if (error.status === 'FETCH_ERROR') {
			return (
				<Alert severity="error" sx={{width: '100%', mb: 2}}>
					<AlertTitle>Network Error</AlertTitle>
					Unable to connect to the server. Please check your connection.
				</Alert>
			);
		}
 
		// Handle other HTTP errors
		return (
			<Alert severity="error" sx={{width: '100%', mb: 2}}>
				<AlertTitle>Error</AlertTitle>
				{fallbackMessage}
			</Alert>
		);
	}
 
// Handle SerializedError (unexpected errors)
	if ('message' in error && typeof error.message === 'string') {
		return (
			<Alert severity="error" sx={{width: '100%', mb: 2}}>
				<AlertTitle>Error</AlertTitle>
				{error.message}
			</Alert>
		);
	}
 
// Fallback for completely unknown error shapes
	return (
		<Alert severity="error" sx={{width: '100%', mb: 2}}>
			<AlertTitle>Error</AlertTitle>
			{fallbackMessage}
		</Alert>
	);
}