All files / src/web/components/InfiniteScroll LoadingSentinel.tsx

100% Statements 85/85
93.75% Branches 15/16
100% Functions 1/1
100% Lines 85/85

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  1x                               1x 6x 6x 6x 6x 6x 6x 6x 6x   6x 1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x   1x   1x     6x 2x 2x 2x 2x 2x 2x 2x 2x   2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 2x 2x 1x 1x 1x   2x   2x     6x 2x 2x 2x 2x 2x 1x 1x 1x   2x   2x     1x 1x 1x 1x 1x   1x   1x  
import React from 'react';
import { Loader2 } from 'lucide-react';
 
export interface LoadingSentinelProps {
  isLoading?: boolean;
  hasReachedEnd?: boolean;
  totalCount?: number;
  loadedCount?: number;
  error?: Error | null;
  onRetry?: () => void;
  className?: string;
}
 
/**
 * Loading sentinel component for infinite scroll
 * Displays loading state, end state, or error state at the bottom of the list
 */
export const LoadingSentinel: React.FC<LoadingSentinelProps> = ({
  isLoading = false,
  hasReachedEnd = false,
  totalCount,
  loadedCount,
  error,
  onRetry,
  className = '',
}) => {
  // Error state
  if (error) {
    return (
      <div className={`flex flex-col items-center justify-center py-8 text-center ${className}`}>
        <div className="text-red-500 mb-4">
          <svg
            className="w-12 h-12 mx-auto mb-2"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
            />
          </svg>
          <p className="text-sm font-medium">Failed to load more newsletters</p>
          <p className="text-xs text-gray-500 mt-1">
            {error.message || 'Something went wrong'}
          </p>
        </div>
        {onRetry && (
          <button
            onClick={onRetry}
            className="px-4 py-2 text-sm bg-red-50 hover:bg-red-100 text-red-700 rounded-lg transition-colors"
          >
            Try Again
          </button>
        )}
      </div>
    );
  }
 
  // End state
  if (hasReachedEnd) {
    return (
      <div className={`flex flex-col items-center justify-center py-8 text-center ${className}`}>
        <div className="text-gray-400 mb-2">
          <svg
            className="w-8 h-8 mx-auto"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M5 13l4 4L19 7"
            />
          </svg>
        </div>
        <p className="text-sm text-gray-500 font-medium">
          {totalCount !== undefined && loadedCount !== undefined
            ? `All ${totalCount} newsletters loaded`
            : 'No more newsletters to load'}
        </p>
        {totalCount !== undefined && loadedCount !== undefined && totalCount > 0 && (
          <p className="text-xs text-gray-400 mt-1">
            Showing {Math.min(loadedCount, totalCount)} of {totalCount} newsletters
          </p>
        )}
      </div>
    );
  }
 
  // Loading state
  if (isLoading) {
    return (
      <div className={`flex flex-col items-center justify-center py-8 ${className}`}>
        <Loader2 className="w-6 h-6 animate-spin text-blue-500 mb-3" />
        <p className="text-sm text-gray-500 font-medium">Loading more newsletters...</p>
        {totalCount !== undefined && loadedCount !== undefined && (
          <p className="text-xs text-gray-400 mt-1">
            Loaded {loadedCount} of {totalCount}
          </p>
        )}
      </div>
    );
  }
 
  // Default invisible sentinel for intersection observer
  return (
    <div
      className={`h-4 w-full ${className}`}
      aria-hidden="true"
    />
  );
};
 
export default LoadingSentinel;