app/components/LoadingStates.tsx (82 lines of code) (raw):

interface LoadingSpinnerProps { size?: "sm" | "md" | "lg"; className?: string; } export const LoadingSpinner = ({ size = "md", className = "", }: LoadingSpinnerProps) => { const sizeClasses = { sm: "w-4 h-4", md: "w-6 h-6", lg: "w-8 h-8", }; return ( <div className={`${sizeClasses[size]} ${className}`}> <svg className="animate-spin text-blue-600" fill="none" viewBox="0 0 24 24" > <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /> </svg> </div> ); }; interface LoadingStateProps { message?: string; } export const LoadingState = ({ message = "Loading..." }: LoadingStateProps) => { return ( <div className="flex flex-col items-center justify-center py-12"> <LoadingSpinner size="lg" className="mb-4" /> <p className="text-gray-600">{message}</p> </div> ); }; interface EmptyStateProps { title: string; description: string; action?: React.ReactNode; icon?: React.ReactNode; } export const EmptyState = ({ title, description, action, icon, }: EmptyStateProps) => { const defaultIcon = ( <svg className="h-12 w-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> </svg> ); return ( <div className="py-12 text-center"> <div className="mb-4">{icon || defaultIcon}</div> <h3 className="mb-2 text-lg font-medium text-gray-900">{title}</h3> <p className="mb-6 text-gray-600">{description}</p> {action} </div> ); };