"use client";
import { useState, useEffect } from "react";
import {
Loader2,
X,
CheckCircle2,
AlertCircle,
Download,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
type LoadingStatus = "loading" | "success" | "error" | "cancelled";
type LoadingStep = {
message: string;
progress: number;
};
const loadingSteps: LoadingStep[] = [
{ message: "Initializing...", progress: 0 },
{ message: "Connecting to server...", progress: 15 },
{ message: "Fetching data...", progress: 30 },
{ message: "Processing records...", progress: 50 },
{ message: "Validating results...", progress: 70 },
{ message: "Finalizing...", progress: 90 },
{ message: "Complete!", progress: 100 },
];
export const title = "React Dialog Block Loading";
export default function DialogLoading() {
const [open, setOpen] = useState(false);
const [status, setStatus] = useState<LoadingStatus>("loading");
const [currentStep, setCurrentStep] = useState(0);
const [progress, setProgress] = useState(0);
const [elapsedTime, setElapsedTime] = useState(0);
// Reset state when dialog opens
useEffect(() => {
if (open) {
setCurrentStep(0);
setProgress(0);
setElapsedTime(0);
setStatus("loading");
}
}, [open]);
// Simulate loading progress
useEffect(() => {
if (!open || status !== "loading") return;
const progressInterval = setInterval(() => {
setCurrentStep((prev) => {
if (prev >= loadingSteps.length - 1) {
setStatus("success");
return prev;
}
return prev + 1;
});
}, 1200);
return () => clearInterval(progressInterval);
}, [open, status]);
// Update progress smoothly based on current step
useEffect(() => {
if (!open || status !== "loading") return;
const targetProgress = loadingSteps[currentStep]?.progress || 0;
const smoothProgress = setInterval(() => {
setProgress((prev) => {
if (prev < targetProgress) {
return Math.min(prev + 2, targetProgress);
}
clearInterval(smoothProgress);
return prev;
});
}, 50);
return () => clearInterval(smoothProgress);
}, [open, status, currentStep]);
// Track elapsed time
useEffect(() => {
if (!open || status !== "loading") return;
const timeInterval = setInterval(() => {
setElapsedTime((prev) => prev + 1);
}, 1000);
return () => clearInterval(timeInterval);
}, [open, status]);
const handleCancel = () => {
setStatus("cancelled");
};
const handleClose = () => {
setOpen(false);
setTimeout(() => {
setStatus("loading");
setCurrentStep(0);
setProgress(0);
setElapsedTime(0);
}, 200);
};
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
};
const currentMessage = loadingSteps[currentStep]?.message || "Processing...";
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<Download className="mr-2 h-4 w-4" />
Start Operation
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{status === "loading" && (
<>
<Loader2 className="h-5 w-5 animate-spin" />
Processing
</>
)}
{status === "success" && (
<>
<CheckCircle2 className="h-5 w-5 text-primary" />
Complete
</>
)}
{status === "error" && (
<>
<AlertCircle className="h-5 w-5 text-destructive" />
Error
</>
)}
{status === "cancelled" && (
<>
<X className="h-5 w-5 text-muted-foreground" />
Cancelled
</>
)}
</DialogTitle>
<DialogDescription>
{status === "loading" && "Please wait while we process your request."}
{status === "success" && "Your operation completed successfully."}
{status === "error" && "Something went wrong during the operation."}
{status === "cancelled" && "The operation was cancelled."}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{status === "loading" && (
<>
{/* Progress Bar */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{currentMessage}</span>
<span className="font-medium">{Math.round(progress)}%</span>
</div>
<Progress value={progress} className="h-2" />
</div>
{/* Steps indicator */}
<div className="flex justify-center gap-1">
{loadingSteps.slice(0, -1).map((_, index) => (
<div
key={index}
className={cn(
"h-1.5 w-6 rounded-full transition-colors",
index <= currentStep ? "bg-primary" : "bg-muted"
)}
/>
))}
</div>
{/* Elapsed Time */}
<div className="flex items-center justify-center">
<span className="text-xs text-muted-foreground">
Elapsed: {formatTime(elapsedTime)}
</span>
</div>
{/* Cancel Button */}
<div className="flex justify-center">
<Button variant="ghost" size="sm" onClick={handleCancel}>
Cancel
</Button>
</div>
</>
)}
{status === "success" && (
<div className="flex flex-col items-center justify-center py-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full border-2 border-primary">
<CheckCircle2 className="h-8 w-8 text-primary" />
</div>
<p className="mt-4 text-sm text-muted-foreground">
Completed in {formatTime(elapsedTime)}
</p>
<Button onClick={handleClose} className="mt-4">
Done
</Button>
</div>
)}
{status === "error" && (
<div className="flex flex-col items-center justify-center py-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full border-2 border-destructive">
<AlertCircle className="h-8 w-8 text-destructive" />
</div>
<p className="mt-4 text-sm font-medium">Operation Failed</p>
<p className="mt-1 text-sm text-muted-foreground">
An unexpected error occurred. Please try again.
</p>
<div className="mt-4 flex gap-2">
<Button variant="outline" onClick={handleClose}>
Close
</Button>
<Button onClick={() => setStatus("loading")}>
Retry
</Button>
</div>
</div>
)}
{status === "cancelled" && (
<div className="flex flex-col items-center justify-center py-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full border-2 border-muted">
<X className="h-8 w-8 text-muted-foreground" />
</div>
<p className="mt-4 text-sm text-muted-foreground">
Operation was cancelled after {formatTime(elapsedTime)}
</p>
<div className="mt-4 flex gap-2">
<Button variant="outline" onClick={handleClose}>
Close
</Button>
<Button onClick={() => setStatus("loading")}>
Start Again
</Button>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}