forked from Nova-reward/Nova-Rewards
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathConfirmationModal.js
More file actions
117 lines (109 loc) · 3.72 KB
/
Copy pathConfirmationModal.js
File metadata and controls
117 lines (109 loc) · 3.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
'use client';
import { useState, useEffect, useRef } from 'react';
import Modal from './modal/Modal';
/**
* ConfirmationModal — prevents accidental destructive actions.
*
* - Confirm button is styled red (destructive) and cancel has default focus.
* - For high-value transfers, pass `requireTypedConfirmation` with the amount
* string; the user must type it before the confirm button enables.
* - Dismissible via Escape and backdrop click (handled by base Modal).
*
* @param {{
* isOpen: boolean,
* onClose: () => void,
* onConfirm: () => void,
* title: string,
* description: string,
* confirmText?: string,
* cancelText?: string,
* requireTypedConfirmation?: string, // e.g. "500" — user must type this
* loading?: boolean,
* }} props
*/
export default function ConfirmationModal({
isOpen,
onClose,
onConfirm,
title,
description,
confirmText = 'Confirm',
cancelText = 'Cancel',
requireTypedConfirmation = null,
loading = false,
}) {
const [typedValue, setTypedValue] = useState('');
const cancelRef = useRef(null);
// Reset typed input when modal opens/closes
useEffect(() => {
if (!isOpen) setTypedValue('');
}, [isOpen]);
// Auto-focus cancel button when modal opens
useEffect(() => {
if (isOpen) {
requestAnimationFrame(() => cancelRef.current?.focus());
}
}, [isOpen]);
const typingRequired = Boolean(requireTypedConfirmation);
const typingMatches = typedValue === String(requireTypedConfirmation);
const canConfirm = !loading && (!typingRequired || typingMatches);
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={title}
size="sm"
closeOnBackdrop={!loading}
aria-describedby="confirm-modal-desc"
footerActions={
<div className="flex gap-3 w-full justify-end">
<button
ref={cancelRef}
className="px-4 py-2 rounded-md text-sm font-medium bg-gray-100 text-gray-800 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 transition-colors disabled:opacity-50"
onClick={onClose}
disabled={loading}
>
{cancelText}
</button>
<button
className="px-4 py-2 rounded-md text-sm font-medium bg-red-600 text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onClick={onConfirm}
disabled={!canConfirm}
aria-disabled={!canConfirm}
>
{loading ? 'Processing…' : confirmText}
</button>
</div>
}
>
<div className="space-y-4">
<p id="confirm-modal-desc" className="text-sm text-gray-600 leading-relaxed">
{description}
</p>
<p className="text-xs font-medium text-red-600 uppercase tracking-wide">
This action cannot be undone.
</p>
{typingRequired && (
<div className="space-y-1">
<label
htmlFor="confirm-type-input"
className="block text-sm text-gray-700"
>
Type <strong>{requireTypedConfirmation}</strong> to confirm
</label>
<input
id="confirm-type-input"
type="text"
value={typedValue}
onChange={(e) => setTypedValue(e.target.value)}
placeholder={String(requireTypedConfirmation)}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-red-400"
autoComplete="off"
aria-label={`Type ${requireTypedConfirmation} to confirm`}
/>
</div>
)}
</div>
</Modal>
);
}