Skip to content

Commit ad19bf8

Browse files
committed
Added pending tx modal loading state
1 parent e4a295a commit ad19bf8

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import React from 'react';
2+
import {
3+
Dialog,
4+
DialogContent,
5+
DialogHeader,
6+
DialogTitle,
7+
DialogDescription,
8+
DialogFooter,
9+
} from '@/components/ui/dialog';
10+
import { Button } from '@/components/ui/button';
11+
import CircularSpinner from '@/components/common/CircularSpinnerProps';
12+
import { cn } from '@/lib/utils';
13+
14+
export interface PendingTxModalProps {
15+
open: boolean;
16+
onOpenChange?: (open: boolean) => void;
17+
/** When true, renders the loading/spinner state */
18+
isLoading?: boolean;
19+
title?: string;
20+
description?: string;
21+
/** Prevent the user from dismissing the modal while loading */
22+
blockDismissal?: boolean;
23+
/** Optional footer action (e.g. "View on explorer") */
24+
action?: {
25+
label: string;
26+
onClick: () => void;
27+
};
28+
}
29+
30+
const PendingTxModal: React.FC<PendingTxModalProps> = ({
31+
open,
32+
onOpenChange,
33+
isLoading = false,
34+
title = 'Transaction pending',
35+
description = 'Your transaction has been submitted and is awaiting confirmation.',
36+
blockDismissal = false,
37+
action,
38+
}) => {
39+
const handleOpenChange = (next: boolean) => {
40+
if (!next && blockDismissal && isLoading) return;
41+
onOpenChange?.(next);
42+
};
43+
44+
return (
45+
<Dialog open={open} onOpenChange={handleOpenChange}>
46+
<DialogContent
47+
showCloseButton={!(blockDismissal && isLoading)}
48+
className="max-w-sm"
49+
// Prevent closing via Escape when dismissal is blocked
50+
onEscapeKeyDown={(e: KeyboardEvent) => {
51+
if (blockDismissal && isLoading) e.preventDefault();
52+
}}
53+
// Prevent closing via overlay click when dismissal is blocked
54+
onInteractOutside={(e: Event) => {
55+
if (blockDismissal && isLoading) e.preventDefault();
56+
}}
57+
>
58+
<DialogHeader>
59+
<div className="mb-2 flex justify-center">
60+
<span
61+
className={cn(
62+
'inline-flex rounded-full p-3',
63+
isLoading
64+
? 'bg-amber-500/15 text-amber-300 ring-1 ring-amber-300/35'
65+
: 'bg-muted text-muted-foreground'
66+
)}
67+
>
68+
{isLoading ? (
69+
<CircularSpinner
70+
size={28}
71+
color="#fcd34d"
72+
speed="0.9s"
73+
bgOpacity={0.15}
74+
/>
75+
) : (
76+
<CircularSpinner
77+
size={28}
78+
color="currentColor"
79+
bgOpacity={0.15}
80+
/>
81+
)}
82+
</span>
83+
</div>
84+
<DialogTitle className="text-center">{title}</DialogTitle>
85+
<DialogDescription className="text-center">
86+
{description}
87+
</DialogDescription>
88+
</DialogHeader>
89+
90+
{(action || !blockDismissal) && (
91+
<DialogFooter className="sm:justify-center">
92+
{action && (
93+
<Button
94+
variant="outline"
95+
size="sm"
96+
onClick={action.onClick}
97+
>
98+
{action.label}
99+
</Button>
100+
)}
101+
{!blockDismissal && (
102+
<Button
103+
variant="ghost"
104+
size="sm"
105+
onClick={() => onOpenChange?.(false)}
106+
>
107+
Dismiss
108+
</Button>
109+
)}
110+
</DialogFooter>
111+
)}
112+
</DialogContent>
113+
</Dialog>
114+
);
115+
};
116+
117+
export default PendingTxModal;

0 commit comments

Comments
 (0)