Skip to content

Commit f8a68b7

Browse files
committed
feat: button confirmation popups
1 parent 9e887ce commit f8a68b7

2 files changed

Lines changed: 292 additions & 12 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React from 'react';
2+
import { X } from 'lucide-react';
3+
import { Button } from './ui/button';
4+
5+
export interface ConfirmationModalProps {
6+
isOpen: boolean;
7+
onClose: () => void;
8+
onConfirm: () => void;
9+
title: string;
10+
heading: React.ReactNode;
11+
description: React.ReactNode;
12+
confirmText?: string;
13+
cancelText?: string;
14+
confirmVariant?:
15+
| 'default'
16+
| 'outline'
17+
| 'secondary'
18+
| 'ghost'
19+
| 'destructive'
20+
| 'success'
21+
| 'share'
22+
| 'link'
23+
| 'unstyled';
24+
isConfirming?: boolean;
25+
position?: { top?: number; right?: number; bottom?: number; left?: number };
26+
}
27+
28+
export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
29+
isOpen,
30+
onClose,
31+
onConfirm,
32+
title,
33+
heading,
34+
description,
35+
confirmText = 'Confirm',
36+
cancelText = 'Cancel',
37+
confirmVariant = 'success',
38+
isConfirming = false,
39+
position,
40+
}) => {
41+
if (!isOpen) return null;
42+
43+
const content = (
44+
<div
45+
className={`bg-white rounded-xl ${position ? 'shadow-2xl border border-[#e5e5e5]' : 'shadow-lg'} w-[440px] max-w-full overflow-hidden`}
46+
>
47+
{/* Header */}
48+
<div className="flex items-center justify-between px-6 pt-6 pb-4">
49+
<h2 className="text-xl font-semibold text-[#171717]">{title}</h2>
50+
<button
51+
onClick={onClose}
52+
className="text-gray-500 hover:text-gray-700 transition-colors"
53+
aria-label="Close modal"
54+
>
55+
<X size={20} strokeWidth={2} />
56+
</button>
57+
</div>
58+
59+
{/* Body */}
60+
<div className="px-6 pb-8">
61+
<h3 className="text-lg font-semibold text-[#171717] mb-2">{heading}</h3>
62+
<div className="text-[15px] leading-relaxed text-[#737373]">
63+
{description}
64+
</div>
65+
</div>
66+
67+
{/* Footer */}
68+
<div className="flex items-center gap-4 px-6 pb-6 mt-2">
69+
<Button
70+
variant="outline"
71+
onClick={onClose}
72+
disabled={isConfirming}
73+
className="flex-1 rounded-[10px] h-12 text-base font-normal border-[#e5e5e5] text-black bg-white hover:bg-gray-50 shadow-sm"
74+
>
75+
{cancelText}
76+
</Button>
77+
<Button
78+
variant={confirmVariant}
79+
onClick={onConfirm}
80+
disabled={isConfirming}
81+
className="flex-1 rounded-[10px] h-12 text-base font-normal"
82+
>
83+
{isConfirming ? 'Loading...' : confirmText}
84+
</Button>
85+
</div>
86+
</div>
87+
);
88+
89+
if (position) {
90+
return (
91+
<>
92+
{/* Invisible backdrop to capture outside clicks and close the popover */}
93+
<div className="fixed inset-0 z-40" onClick={onClose} />
94+
<div
95+
className="fixed z-50 font-['Source_Sans_Pro']"
96+
style={{ ...position, position: 'fixed' }}
97+
>
98+
{content}
99+
</div>
100+
</>
101+
);
102+
}
103+
104+
return (
105+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm font-['Source_Sans_Pro']">
106+
{content}
107+
</div>
108+
);
109+
};

apps/frontend/src/containers/dashboard/UserManagement.tsx

Lines changed: 183 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import apiClient from '../../api/apiClient';
44
import { useAuth } from '../../components/AuthProvider';
55
import { Button } from '../../components/ui/button';
66
import { Input } from '../../components/ui/input';
7+
import { ConfirmationModal } from '../../components/ConfirmationModal';
78

89
interface CombinedUser {
910
username: string;
@@ -67,6 +68,14 @@ export const UserManagement: React.FC = () => {
6768
const [searchQuery, setSearchQuery] = useState('');
6869
const [activeTab, setActiveTab] = useState<'pending' | 'approved'>('pending');
6970
const [currentPage, setCurrentPage] = useState(1);
71+
const [editingUser, setEditingUser] = useState<CombinedUser | null>(null);
72+
const [denyingUser, setDenyingUser] = useState<CombinedUser | null>(null);
73+
const [verifyingUser, setVerifyingUser] = useState<CombinedUser | null>(null);
74+
const [modalPosition, setModalPosition] = useState<
75+
{ top: number; right: number } | undefined
76+
>(undefined);
77+
const [isUpdatingRole, setIsUpdatingRole] = useState(false);
78+
const [isProcessing, setIsProcessing] = useState(false);
7079

7180
const fetchUsers = async () => {
7281
try {
@@ -84,25 +93,60 @@ export const UserManagement: React.FC = () => {
8493
fetchUsers();
8594
}, []);
8695

87-
const handleVerify = async (email: string) => {
96+
const handleVerify = async () => {
97+
if (!verifyingUser) return;
98+
setIsProcessing(true);
8899
try {
89100
await (apiClient as any).axiosInstance.post('/api/auth/admin-verify', {
90-
email,
101+
email: verifyingUser.email,
91102
});
92-
fetchUsers();
103+
await fetchUsers();
104+
setVerifyingUser(null);
105+
setModalPosition(undefined);
93106
} catch (err: any) {
94107
alert('Error: ' + err.message);
108+
} finally {
109+
setIsProcessing(false);
95110
}
96111
};
97112

98-
const handleDeny = async (email: string) => {
113+
const handleDeny = async () => {
114+
if (!denyingUser) return;
115+
setIsProcessing(true);
99116
try {
100117
await (apiClient as any).axiosInstance.post('/api/auth/admin-deny', {
101-
email,
118+
email: denyingUser.email,
102119
});
103-
fetchUsers();
120+
await fetchUsers();
121+
setDenyingUser(null);
122+
setModalPosition(undefined);
104123
} catch (err: any) {
105124
alert('Error: ' + err.message);
125+
} finally {
126+
setIsProcessing(false);
127+
}
128+
};
129+
130+
const handleEditRole = async () => {
131+
if (!editingUser) return;
132+
133+
// Shell function: we'll just log and close the modal for now
134+
// as per instructions: "build it for edit role as an example"
135+
setIsUpdatingRole(true);
136+
try {
137+
console.log(`Updating role for ${editingUser.email}`);
138+
// Replace with your API call:
139+
// await apiClient.axiosInstance.patch('/api/auth/user/role', { ... })
140+
141+
// Artificial delay to show loading state if you have one
142+
await new Promise((resolve) => setTimeout(resolve, 500));
143+
144+
await fetchUsers();
145+
setEditingUser(null);
146+
} catch (err: any) {
147+
alert('Error changing role: ' + err.message);
148+
} finally {
149+
setIsUpdatingRole(false);
106150
}
107151
};
108152

@@ -249,15 +293,31 @@ export const UserManagement: React.FC = () => {
249293
<>
250294
<Button
251295
variant="success"
252-
onClick={() => handleVerify(user.email)}
296+
onClick={(e) => {
297+
const rect =
298+
e.currentTarget.getBoundingClientRect();
299+
setModalPosition({
300+
top: rect.bottom + 8,
301+
right: window.innerWidth - rect.right,
302+
});
303+
setVerifyingUser(user);
304+
}}
253305
className="rounded-[10px] px-3 py-1 h-auto text-sm leading-6"
254306
>
255307
Approve
256308
</Button>
257309
<Button
258310
variant="outline"
259-
onClick={() => handleDeny(user.email)}
260-
className="rounded-[10px] px-3 py-1 h-auto text-sm leading-6 border-[#e5e5e5] text-black bg-white hover:bg-gray-50"
311+
onClick={(e) => {
312+
const rect =
313+
e.currentTarget.getBoundingClientRect();
314+
setModalPosition({
315+
top: rect.bottom + 8,
316+
right: window.innerWidth - rect.right,
317+
});
318+
setDenyingUser(user);
319+
}}
320+
className="rounded-[10px] px-3 py-1 h-auto text-sm leading-6 border-[#e5e5e5] text-black bg-white hover:bg-gray-50 shadow-sm"
261321
>
262322
Deny
263323
</Button>
@@ -266,13 +326,30 @@ export const UserManagement: React.FC = () => {
266326
<>
267327
<Button
268328
variant="outline"
269-
className="rounded-[10px] px-3 py-1 h-auto text-sm leading-6 font-['Source_Sans_Pro'] border-[#e5e5e5] text-black bg-white hover:bg-gray-50"
329+
onClick={(e) => {
330+
const rect =
331+
e.currentTarget.getBoundingClientRect();
332+
setModalPosition({
333+
top: rect.bottom + 8,
334+
right: window.innerWidth - rect.right,
335+
});
336+
setEditingUser(user);
337+
}}
338+
className="rounded-[10px] px-3 py-1 h-auto text-sm leading-6 font-['Source_Sans_Pro'] border-[#e5e5e5] text-black bg-white hover:bg-gray-50 shadow-sm"
270339
>
271340
Edit Role
272341
</Button>
273342
<Button
274-
onClick={() => handleDeny(user.email)}
275-
className="rounded-[10px] px-3 py-1 h-auto text-sm leading-6 bg-[#893C27] text-white hover:bg-[#6c2f1f] border-0"
343+
onClick={(e) => {
344+
const rect =
345+
e.currentTarget.getBoundingClientRect();
346+
setModalPosition({
347+
top: rect.bottom + 8,
348+
right: window.innerWidth - rect.right,
349+
});
350+
setDenyingUser(user);
351+
}}
352+
className="rounded-[10px] px-3 py-1 h-auto text-sm leading-6 bg-[#893C27] text-white hover:bg-[#6c2f1f] border-0 outline-none"
276353
>
277354
Delete User
278355
</Button>
@@ -343,6 +420,100 @@ export const UserManagement: React.FC = () => {
343420
</Button>
344421
</div>
345422
)}
423+
424+
{/* Modular Popups for User Actions */}
425+
{editingUser && (
426+
<ConfirmationModal
427+
isOpen={true}
428+
position={modalPosition}
429+
onClose={() => {
430+
setEditingUser(null);
431+
setModalPosition(undefined);
432+
}}
433+
onConfirm={handleEditRole}
434+
title="Edit Role"
435+
heading={<>Update {editingUser.name || editingUser.username} role?</>}
436+
description={
437+
<>
438+
By pressing Confirm, you will update{' '}
439+
<span className="text-[#171717]">
440+
{editingUser.name || editingUser.username}
441+
</span>{' '}
442+
role to{' '}
443+
<span className="text-[#171717]">
444+
{editingUser.dbUser?.status === 'ADMIN' ? 'STANDARD' : 'ADMIN'}
445+
</span>
446+
.
447+
</>
448+
}
449+
confirmText="Confirm"
450+
cancelText="Cancel"
451+
confirmVariant="success"
452+
isConfirming={isUpdatingRole}
453+
/>
454+
)}
455+
456+
{verifyingUser && (
457+
<ConfirmationModal
458+
isOpen={true}
459+
position={modalPosition}
460+
onClose={() => {
461+
setVerifyingUser(null);
462+
setModalPosition(undefined);
463+
}}
464+
onConfirm={handleVerify}
465+
title="Approve User"
466+
heading={<>Approve {verifyingUser.name || verifyingUser.username}?</>}
467+
description={
468+
<>
469+
By pressing Confirm, you will approve{' '}
470+
<span className="text-[#171717]">
471+
{verifyingUser.name || verifyingUser.username}
472+
</span>{' '}
473+
as a user. This will allow them to access the platform.
474+
</>
475+
}
476+
confirmText="Confirm"
477+
cancelText="Cancel"
478+
confirmVariant="success"
479+
isConfirming={isProcessing}
480+
/>
481+
)}
482+
483+
{denyingUser && (
484+
<ConfirmationModal
485+
isOpen={true}
486+
position={modalPosition}
487+
onClose={() => {
488+
setDenyingUser(null);
489+
setModalPosition(undefined);
490+
}}
491+
onConfirm={handleDeny}
492+
title="Delete User"
493+
heading={
494+
<>
495+
{activeTab === 'pending' ? 'Deny' : 'Delete'}{' '}
496+
{denyingUser.name || denyingUser.username}?
497+
</>
498+
}
499+
description={
500+
<>
501+
By pressing Confirm, you will{' '}
502+
<span className="text-[#171717]">
503+
{activeTab === 'pending' ? 'deny and remove' : 'delete'}
504+
</span>{' '}
505+
<span className="text-[#171717]">
506+
{denyingUser.name || denyingUser.username}
507+
</span>{' '}
508+
from the system. This action cannot be undone.
509+
</>
510+
}
511+
confirmText="Confirm"
512+
cancelText="Cancel"
513+
confirmVariant="destructive"
514+
isConfirming={isProcessing}
515+
/>
516+
)}
346517
</div>
347518
);
348519
};

0 commit comments

Comments
 (0)