From dc3210927a5acc7891ed0757003ada3fa268c594 Mon Sep 17 00:00:00 2001 From: poduck Date: Tue, 2 Dec 2025 11:26:47 -0500 Subject: [PATCH] feat(platform): Add confirmation modal for email verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create reusable ConfirmationModal component with variants (info, warning, danger, success) - Replace browser confirm() dialogs with styled modal for email verification - Update PlatformBusinesses and PlatformUsers to use the new modal - Add translation keys for verification messages - Unverify test@example.com for testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/src/components/ConfirmationModal.tsx | 131 ++++++++++++++++++ frontend/src/i18n/locales/en.json | 2 + .../src/pages/platform/PlatformBusinesses.tsx | 59 ++++++-- frontend/src/pages/platform/PlatformUsers.tsx | 53 +++++-- 4 files changed, 226 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/ConfirmationModal.tsx diff --git a/frontend/src/components/ConfirmationModal.tsx b/frontend/src/components/ConfirmationModal.tsx new file mode 100644 index 0000000..7198366 --- /dev/null +++ b/frontend/src/components/ConfirmationModal.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { X, AlertTriangle, CheckCircle, Info, AlertCircle } from 'lucide-react'; + +type ModalVariant = 'info' | 'warning' | 'danger' | 'success'; + +interface ConfirmationModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + message: string | React.ReactNode; + confirmText?: string; + cancelText?: string; + variant?: ModalVariant; + isLoading?: boolean; +} + +const variantConfig: Record = { + info: { + icon: , + iconBg: 'bg-blue-100 dark:bg-blue-900/30', + confirmButtonClass: 'bg-blue-600 hover:bg-blue-700 text-white', + }, + warning: { + icon: , + iconBg: 'bg-amber-100 dark:bg-amber-900/30', + confirmButtonClass: 'bg-amber-600 hover:bg-amber-700 text-white', + }, + danger: { + icon: , + iconBg: 'bg-red-100 dark:bg-red-900/30', + confirmButtonClass: 'bg-red-600 hover:bg-red-700 text-white', + }, + success: { + icon: , + iconBg: 'bg-green-100 dark:bg-green-900/30', + confirmButtonClass: 'bg-green-600 hover:bg-green-700 text-white', + }, +}; + +const ConfirmationModal: React.FC = ({ + isOpen, + onClose, + onConfirm, + title, + message, + confirmText = 'Confirm', + cancelText = 'Cancel', + variant = 'info', + isLoading = false, +}) => { + if (!isOpen) return null; + + const config = variantConfig[variant]; + + const handleConfirm = () => { + onConfirm(); + }; + + return ( +
+
+ {/* Modal Header */} +
+
+
+ {config.icon} +
+

{title}

+
+ +
+ + {/* Modal Body */} +
+
+ {typeof message === 'string' ?

{message}

: message} +
+
+ + {/* Modal Footer */} +
+ + +
+
+
+ ); +}; + +export default ConfirmationModal; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index fefa46e..1b4f12b 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -602,6 +602,8 @@ "verifyEmail": "Verify Email", "verify": "Verify", "confirmVerifyEmail": "Are you sure you want to manually verify this user's email?", + "confirmVerifyEmailMessage": "Are you sure you want to manually verify this user's email address?", + "verifyEmailNote": "This will mark their email as verified and allow them to access all features that require email verification.", "noUsersFound": "No users found matching your filters.", "roles": { "superuser": "Superuser", diff --git a/frontend/src/pages/platform/PlatformBusinesses.tsx b/frontend/src/pages/platform/PlatformBusinesses.tsx index 8fc8631..e48ce71 100644 --- a/frontend/src/pages/platform/PlatformBusinesses.tsx +++ b/frontend/src/pages/platform/PlatformBusinesses.tsx @@ -9,6 +9,7 @@ import PlatformListing from './components/PlatformListing'; import PlatformTable from './components/PlatformTable'; import PlatformListRow from './components/PlatformListRow'; import EditPlatformEntityModal from './components/EditPlatformEntityModal'; +import ConfirmationModal from '../../components/ConfirmationModal'; import { useQueryClient } from '@tanstack/react-query'; interface PlatformBusinessesProps { @@ -25,6 +26,8 @@ const PlatformBusinesses: React.FC = ({ onMasquerade }) const [showInviteModal, setShowInviteModal] = useState(false); const [editingBusiness, setEditingBusiness] = useState(null); const [showInactiveBusinesses, setShowInactiveBusinesses] = useState(false); + const [verifyEmailUser, setVerifyEmailUser] = useState<{ id: number; email: string; name: string } | null>(null); + const [isVerifying, setIsVerifying] = useState(false); // Filter and separate businesses const filteredBusinesses = (businesses || []).filter(b => @@ -47,14 +50,22 @@ const PlatformBusinesses: React.FC = ({ onMasquerade }) } }; - const handleVerifyEmail = async (userId: number) => { - if (confirm(t('platform.confirmVerifyEmail'))) { - try { - await verifyUserEmail(userId); - queryClient.invalidateQueries({ queryKey: ['platform', 'businesses'] }); - } catch (error) { - alert(t('errors.generic')); - } + const handleVerifyEmailClick = (userId: number, email: string, name: string) => { + setVerifyEmailUser({ id: userId, email, name }); + }; + + const handleVerifyEmailConfirm = async () => { + if (!verifyEmailUser) return; + + setIsVerifying(true); + try { + await verifyUserEmail(verifyEmailUser.id); + queryClient.invalidateQueries({ queryKey: ['platform', 'businesses'] }); + setVerifyEmailUser(null); + } catch (error) { + alert(t('errors.generic')); + } finally { + setIsVerifying(false); } }; @@ -70,9 +81,13 @@ const PlatformBusinesses: React.FC = ({ onMasquerade }) tertiaryText={business.owner?.email || '-'} actions={ <> - {business.owner && !business.owner.email_verified && ( // Assuming PlatformBusiness owner object has email_verified, if not we might need to fetch it or update interface + {business.owner && !business.owner.email_verified && (