11"use client" ;
2- import { buttonVariants } from "@/components/ui/button" ;
2+
3+ import { Button , buttonVariants } from "@/components/ui/button" ;
34import {
45 Dialog ,
56 DialogContent ,
@@ -10,7 +11,14 @@ import {
1011} from "@/components/ui/dialog" ;
1112import { ScrollArea , ScrollBar } from "@/components/ui/scroll-area" ;
1213import { 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" ;
1422import 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
2434export type StepState = "idle" | "active" | "completed" | "error" ;
2535
@@ -32,7 +42,77 @@ export type DialogStep = {
3242
3343export 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+
35114export 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