368 lines
8.3 KiB
TypeScript
368 lines
8.3 KiB
TypeScript
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;
|