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>
);
};