inogen/app/components/ui/loading.tsx

368 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from "react";
import { cn } from "~/lib/utils";
import { Loader2 } from "lucide-react";
interface LoadingSpinnerProps {
size?: "xs" | "sm" | "md" | "lg" | "xl";
variant?: "primary" | "secondary" | "muted" | "white";
className?: string;
}
export function LoadingSpinner({
size = "md",
variant = "primary",
className,
}: LoadingSpinnerProps) {
const sizes = {
xs: "w-3 h-3",
sm: "w-4 h-4",
md: "w-6 h-6",
lg: "w-8 h-8",
xl: "w-12 h-12",
};
const variants = {
primary: "text-primary",
secondary: "text-secondary",
muted: "text-muted-foreground",
white: "text-white",
};
return (
<Loader2
className={cn("animate-spin", sizes[size], variants[variant], className)}
/>
);
}
interface LoadingDotsProps {
size?: "sm" | "md" | "lg";
variant?: "primary" | "secondary" | "muted" | "white";
className?: string;
}
export function LoadingDots({
size = "md",
variant = "primary",
className,
}: LoadingDotsProps) {
const sizes = {
sm: "w-1 h-1",
md: "w-2 h-2",
lg: "w-3 h-3",
};
const variants = {
primary: "bg-primary",
secondary: "bg-secondary",
muted: "bg-muted-foreground",
white: "bg-white",
};
const dotClass = cn(
"rounded-full animate-pulse",
sizes[size],
variants[variant],
);
return (
<div className={cn("flex items-center space-x-1", className)}>
<div className={cn(dotClass, "animation-delay-0")} />
<div className={cn(dotClass, "animation-delay-200")} />
<div className={cn(dotClass, "animation-delay-400")} />
</div>
);
}
interface LoadingBarProps {
progress?: number;
variant?: "primary" | "secondary" | "success" | "warning" | "error";
size?: "sm" | "md" | "lg";
className?: string;
showPercentage?: boolean;
}
export function LoadingBar({
progress,
variant = "primary",
size = "md",
className,
showPercentage = false,
}: LoadingBarProps) {
const variants = {
primary: "bg-primary",
secondary: "bg-secondary",
success: "bg-green-500",
warning: "bg-yellow-500",
error: "bg-red-500",
};
const sizes = {
sm: "h-1",
md: "h-2",
lg: "h-3",
};
return (
<div className={cn("w-full", className)}>
{showPercentage && progress !== undefined && (
<div className="flex justify-between text-sm text-muted-foreground mb-1 font-persian">
<span>در حال بارگذاری...</span>
<span>{Math.round(progress)}%</span>
</div>
)}
<div
className={cn(
"w-full bg-muted rounded-full overflow-hidden",
sizes[size],
)}
>
<div
className={cn(
"h-full transition-all duration-300 ease-out rounded-full",
variants[variant],
progress === undefined && "animate-pulse",
)}
style={{
width:
progress !== undefined ? `${Math.min(progress, 100)}%` : "100%",
}}
/>
</div>
</div>
);
}
interface LoadingPageProps {
title?: string;
description?: string;
variant?: "primary" | "white";
className?: string;
}
export function LoadingPage({
title = "در حال بارگذاری...",
description,
variant = "primary",
className,
}: LoadingPageProps) {
const isWhite = variant === "white";
return (
<div
className={cn(
"min-h-screen flex items-center justify-center",
isWhite ? "bg-background" : "bg-slate-800",
className,
)}
>
<div className="text-center space-y-6 max-w-md mx-auto p-8">
<div className="flex justify-center">
<LoadingSpinner size="xl" variant={isWhite ? "primary" : "white"} />
</div>
<div className="space-y-2">
<h2
className={cn(
"text-lg font-medium font-persian",
isWhite ? "text-foreground" : "text-white",
)}
>
{title}
</h2>
{description && (
<p
className={cn(
"text-sm font-persian leading-relaxed",
isWhite ? "text-muted-foreground" : "text-slate-300",
)}
>
{description}
</p>
)}
</div>
</div>
</div>
);
}
interface LoadingOverlayProps {
visible: boolean;
title?: string;
description?: string;
className?: string;
}
export function LoadingOverlay({
visible,
title = "در حال پردازش...",
description,
className,
}: LoadingOverlayProps) {
if (!visible) return null;
return (
<div
className={cn(
"fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center transition-all duration-200",
className,
)}
>
<div className="bg-card border border-border rounded-lg p-6 max-w-sm mx-4 shadow-lg">
<div className="text-center space-y-4">
<LoadingSpinner size="lg" />
<div className="space-y-1">
<h3 className="text-sm font-medium font-persian text-card-foreground">
{title}
</h3>
{description && (
<p className="text-xs text-muted-foreground font-persian">
{description}
</p>
)}
</div>
</div>
</div>
</div>
);
}
interface LoadingButtonProps {
loading: boolean;
children: React.ReactNode;
className?: string;
}
export function LoadingButton({
loading,
children,
className,
...props
}: LoadingButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
className={cn(
"relative inline-flex items-center justify-center",
className,
)}
disabled={loading}
{...props}
>
{loading && (
<LoadingSpinner
size="sm"
className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2"
/>
)}
<span className={cn(loading && "opacity-0")}>{children}</span>
</button>
);
}
interface LoadingCardProps {
title?: string;
lines?: number;
showAvatar?: boolean;
className?: string;
}
export function LoadingCard({
title,
lines = 3,
showAvatar = false,
className,
}: LoadingCardProps) {
return (
<div className={cn("animate-pulse p-4 space-y-4", className)}>
{title && <div className="h-4 bg-muted rounded w-3/4" />}
<div className="flex items-start space-x-4">
{showAvatar && <div className="w-10 h-10 bg-muted rounded-full" />}
<div className="flex-1 space-y-2">
{Array.from({ length: lines }).map((_, index) => (
<div
key={index}
className={cn(
"h-3 bg-muted rounded",
index === lines - 1 ? "w-2/3" : "w-full",
)}
/>
))}
</div>
</div>
</div>
);
}
interface LoadingSkeletonProps {
className?: string;
variant?: "text" | "circular" | "rectangular";
width?: string | number;
height?: string | number;
}
export function LoadingSkeleton({
className,
variant = "rectangular",
width,
height,
}: LoadingSkeletonProps) {
const variants = {
text: "h-4 w-full",
circular: "rounded-full",
rectangular: "rounded",
};
const style: React.CSSProperties = {};
if (width) style.width = typeof width === "number" ? `${width}px` : width;
if (height)
style.height = typeof height === "number" ? `${height}px` : height;
return (
<div
className={cn(
"animate-pulse bg-muted",
variants[variant],
!width && !height && variants.text,
className,
)}
style={style}
/>
);
}
// Utility component for loading states
interface LoadingStateProps {
loading: boolean;
error?: string | null;
children: React.ReactNode;
loadingComponent?: React.ReactNode;
errorComponent?: React.ReactNode;
}
export function LoadingState({
loading,
error,
children,
loadingComponent,
errorComponent,
}: LoadingStateProps) {
if (loading) {
return loadingComponent || <LoadingSpinner />;
}
if (error) {
return (
errorComponent || (
<div className="text-center p-4">
<p className="text-destructive text-sm font-persian">{error}</p>
</div>
)
);
}
return <>{children}</>;
}
// Default export for convenience
export default LoadingSpinner;