Skip to content

Commit 48e45ad

Browse files
committed
feat(dialog): add toast for step process dialog
This change is intended to allow the user to leave the step dialog during the tx execution(any kind of action) and to check the status on toast. toast is displayed when user closes the dialog
1 parent 73a5443 commit 48e45ad

File tree

1 file changed

+145
-59
lines changed

1 file changed

+145
-59
lines changed

components/global/step-process-dialog.tsx

Lines changed: 145 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
2-
import { buttonVariants } from "@/components/ui/button";
2+
3+
import { Button, buttonVariants } from "@/components/ui/button";
34
import {
45
Dialog,
56
DialogContent,
@@ -10,7 +11,14 @@ import {
1011
} from "@/components/ui/dialog";
1112
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
1213
import { cn } from "@/lib/utils";
13-
import { AlertCircle, Badge, BadgeCheck, Loader } from "lucide-react";
14+
import {
15+
AlertCircle,
16+
Badge,
17+
BadgeCheck,
18+
Loader,
19+
CheckCircle2,
20+
XCircle,
21+
} from "lucide-react";
1422
import React, {
1523
createContext,
1624
createElement,
@@ -20,6 +28,8 @@ import React, {
2028
useEffect,
2129
useState,
2230
} from "react";
31+
import { useToast } from "@/components/ui/use-toast";
32+
import { ToastAction } from "@/components/ui/toast";
2333

2434
export type StepState = "idle" | "active" | "completed" | "error";
2535

@@ -32,7 +42,77 @@ export type DialogStep = {
3242

3343
export type StepData = Pick<DialogStep, "id" | "description">;
3444

45+
const ToastStepper = () => {
46+
const { toast } = useToast();
47+
const { dialogSteps, setOpen, open } = useStepProcessDialogContext();
48+
49+
useEffect(() => {
50+
if (open) return;
51+
52+
const activeStep = dialogSteps.find((step) => step.state === "active");
53+
const errorStep = dialogSteps.find((step) => step.state === "error");
54+
const lastStep = dialogSteps[dialogSteps.length - 1];
55+
const isComplete = lastStep?.state === "completed";
56+
57+
if (activeStep) {
58+
// Show toast while action is in progress
59+
toast({
60+
duration: Infinity, // Keep toast visible until completes
61+
description: (
62+
<div className="flex items-center flex-row">
63+
<span>
64+
<Loader className="h-4 w-4 animate-spin mr-2" />
65+
</span>
66+
{activeStep.description}
67+
</div>
68+
),
69+
action: (
70+
<Button type="button" variant="outline" onClick={() => setOpen(true)}>
71+
View Progress
72+
</Button>
73+
),
74+
});
75+
} else if (errorStep) {
76+
// Show error toast
77+
toast({
78+
duration: 5000,
79+
variant: "destructive",
80+
title: "Failed",
81+
description: errorStep.description,
82+
action: (
83+
<ToastAction altText="View Details" onClick={() => setOpen(true)}>
84+
<span>
85+
<XCircle className="h-4 w-4 mr-2" />
86+
</span>
87+
View Details
88+
</ToastAction>
89+
),
90+
});
91+
} else if (isComplete) {
92+
// Show success toast that auto-dismisses after 5 seconds
93+
toast({
94+
duration: 5000,
95+
title: "Completed",
96+
description: (
97+
<div className="flex items-center flex-row">
98+
<CheckCircle2 className="h-4 w-4 text-green-600 mr-2" />
99+
{lastStep.description}
100+
</div>
101+
),
102+
action: (
103+
<ToastAction altText="View Details" onClick={() => setOpen(true)}>
104+
View Details
105+
</ToastAction>
106+
),
107+
});
108+
}
109+
}, [dialogSteps, setOpen, toast, open]);
110+
111+
return null;
112+
};
113+
35114
export const StepProcessDialogContext = createContext<{
115+
open: boolean;
36116
setDialogStep: (
37117
step: DialogStep["id"],
38118
newState?: StepState,
@@ -44,6 +124,7 @@ export const StepProcessDialogContext = createContext<{
44124
dialogSteps: DialogStep[];
45125
setExtraContent: React.Dispatch<React.SetStateAction<React.ReactNode>>;
46126
}>({
127+
open: false,
47128
setDialogStep: async () => Promise.resolve(),
48129
setSteps: () => {},
49130
setOpen: () => {},
@@ -126,6 +207,7 @@ export const StepProcessDialogProvider = ({
126207
return (
127208
<StepProcessDialogContext.Provider
128209
value={{
210+
open,
129211
setDialogStep,
130212
setSteps,
131213
setOpen,
@@ -198,71 +280,75 @@ const StepProcessModal = ({
198280
const isLastStepCompleted = lastStep?.state === "completed";
199281

200282
return (
201-
<Dialog open={open} onOpenChange={onOpenChange} modal>
202-
{triggerLabel && (
203-
<DialogTrigger
204-
asChild
205-
className={buttonVariants({ variant: "secondary" })}
206-
>
207-
{triggerLabel}
208-
</DialogTrigger>
209-
)}
210-
<DialogContent className="max-w-[500px]">
211-
<DialogHeader>
212-
<DialogTitle className="font-serif text-3xl font-normal">
213-
{title}
214-
</DialogTitle>
215-
</DialogHeader>
216-
<DialogDescription hidden>
217-
Shows the status of the transaction
218-
</DialogDescription>
219-
<div className="flex flex-col px-2 pt-3">
220-
{steps.map((step, index) => (
221-
<div
222-
key={step.id}
223-
className="flex items-center relative border-l-2 border-slate-300 pl-2 pb-6 last-of-type:pb-0"
224-
>
283+
<>
284+
<ToastStepper />
285+
286+
<Dialog open={open} onOpenChange={onOpenChange} modal>
287+
{triggerLabel && (
288+
<DialogTrigger
289+
asChild
290+
className={buttonVariants({ variant: "secondary" })}
291+
>
292+
{triggerLabel}
293+
</DialogTrigger>
294+
)}
295+
<DialogContent className="max-w-[500px]">
296+
<DialogHeader>
297+
<DialogTitle className="font-serif text-3xl font-normal">
298+
{title}
299+
</DialogTitle>
300+
</DialogHeader>
301+
<DialogDescription hidden>
302+
Shows the status of the transaction
303+
</DialogDescription>
304+
<div className="flex flex-col px-2 pt-3">
305+
{steps.map((step, index) => (
225306
<div
226-
className={cn(
227-
"p-1 absolute -left-[14px] top-[2px] bg-slate-100 rounded-full",
228-
stateSpecificIconClasses[step.state],
229-
step === lastStep &&
230-
isLastStepCompleted &&
231-
"text-green-600 bg-green-100",
232-
)}
307+
key={step.id}
308+
className="flex items-center relative border-l-2 border-slate-300 pl-2 pb-6 last-of-type:pb-0"
233309
>
234-
{createElement(stepStateIcons[step.state], {
235-
size: 18,
236-
})}
237-
</div>
238-
<div className="flex flex-col pl-4 justify-center">
239-
<p
310+
<div
240311
className={cn(
241-
"text-lg",
242-
stateSpecificTextClasses[step.state],
312+
"p-1 absolute -left-[14px] top-[2px] bg-slate-100 rounded-full",
313+
stateSpecificIconClasses[step.state],
243314
step === lastStep &&
244315
isLastStepCompleted &&
245-
"text-green-600",
316+
"text-green-600 bg-green-100",
246317
)}
247318
>
248-
{step.description}
249-
</p>
250-
{step.state === "error" && step.errorMessage && (
251-
<ScrollArea className="w-96 h-16 rounded p-2 bg-red-50">
252-
<p className="text-red-500 text-xs font-mono">
253-
({step.errorMessage})
254-
</p>
255-
<ScrollBar orientation="horizontal" />
256-
</ScrollArea>
257-
)}
319+
{createElement(stepStateIcons[step.state], {
320+
size: 18,
321+
})}
322+
</div>
323+
<div className="flex flex-col pl-4 justify-center">
324+
<p
325+
className={cn(
326+
"text-lg",
327+
stateSpecificTextClasses[step.state],
328+
step === lastStep &&
329+
isLastStepCompleted &&
330+
"text-green-600",
331+
)}
332+
>
333+
{step.description}
334+
</p>
335+
{step.state === "error" && step.errorMessage && (
336+
<ScrollArea className="w-96 h-16 rounded p-2 bg-red-50">
337+
<p className="text-red-500 text-xs font-mono">
338+
({step.errorMessage})
339+
</p>
340+
<ScrollBar orientation="horizontal" />
341+
</ScrollArea>
342+
)}
343+
</div>
258344
</div>
259-
</div>
260-
))}
261-
</div>
262-
<div className="flex flex-col px-2 pt-3">{extraContent}</div>
263-
</DialogContent>
264-
</Dialog>
345+
))}
346+
</div>
347+
<div className="flex flex-col px-2 pt-3">{extraContent}</div>
348+
</DialogContent>
349+
</Dialog>
350+
</>
265351
);
266352
};
267353

268-
export { StepProcessModal as default };
354+
export { StepProcessModal as default, ToastStepper };

0 commit comments

Comments
 (0)