refactor(frontend): replace confirm() with modern confirmation modals

Replaced all confirm() dialogs with reusable ConfirmationModal component
for better UX. Modal dialogs provide clearer context, visual consistency,
and prevent accidental confirmations.

Changes:
- MatchesPage: Reject match request confirmation modal
- DashboardPage: Decline and cancel request confirmation modals
- ContactMessagesPage: Delete message confirmation modal

All modals support loading states during async operations and provide
clear action descriptions with destructive action styling.
This commit is contained in:
Radosław Gierwiało
2025-12-05 22:14:09 +01:00
parent bb8a876ab0
commit 76be8a4419
3 changed files with 122 additions and 21 deletions

View File

@@ -7,6 +7,7 @@ import { dashboardAPI, matchesAPI } from '../services/api';
import { connectSocket, disconnectSocket, getSocket } from '../services/socket'; import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
import { DashboardSkeleton } from '../components/common/Skeleton'; import { DashboardSkeleton } from '../components/common/Skeleton';
import EmptyState from '../components/common/EmptyState'; import EmptyState from '../components/common/EmptyState';
import ConfirmationModal from '../components/modals/ConfirmationModal';
import { import {
DashboardEventCard, DashboardEventCard,
DashboardMatchCard, DashboardMatchCard,
@@ -28,6 +29,9 @@ const DashboardPage = () => {
const { user } = useAuth(); const { user } = useAuth();
const [data, setData] = useState(null); const [data, setData] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showDeclineModal, setShowDeclineModal] = useState(false);
const [showCancelModal, setShowCancelModal] = useState(false);
const [matchToProcess, setMatchToProcess] = useState(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [processingMatchId, setProcessingMatchId] = useState(null); const [processingMatchId, setProcessingMatchId] = useState(null);
@@ -105,12 +109,17 @@ const DashboardPage = () => {
} }
}; };
const handleRejectMatch = async (matchSlug) => { const handleRejectMatch = (matchSlug) => {
if (!confirm('Are you sure you want to decline this request?')) return; setMatchToProcess(matchSlug);
setShowDeclineModal(true);
};
const confirmRejectMatch = async () => {
if (!matchToProcess) return;
try { try {
setProcessingMatchId(matchSlug); setProcessingMatchId(matchToProcess);
await matchesAPI.deleteMatch(matchSlug); await matchesAPI.deleteMatch(matchToProcess);
toast.success('Request declined.'); toast.success('Request declined.');
await loadDashboard(); await loadDashboard();
} catch (err) { } catch (err) {
@@ -118,15 +127,22 @@ const DashboardPage = () => {
toast.error('Failed to decline request. Please try again.'); toast.error('Failed to decline request. Please try again.');
} finally { } finally {
setProcessingMatchId(null); setProcessingMatchId(null);
setShowDeclineModal(false);
setMatchToProcess(null);
} }
}; };
const handleCancelRequest = async (matchSlug) => { const handleCancelRequest = (matchSlug) => {
if (!confirm('Are you sure you want to cancel this request?')) return; setMatchToProcess(matchSlug);
setShowCancelModal(true);
};
const confirmCancelRequest = async () => {
if (!matchToProcess) return;
try { try {
setProcessingMatchId(matchSlug); setProcessingMatchId(matchToProcess);
await matchesAPI.deleteMatch(matchSlug); await matchesAPI.deleteMatch(matchToProcess);
toast.success('Request cancelled.'); toast.success('Request cancelled.');
await loadDashboard(); await loadDashboard();
} catch (err) { } catch (err) {
@@ -134,6 +150,8 @@ const DashboardPage = () => {
toast.error('Failed to cancel request. Please try again.'); toast.error('Failed to cancel request. Please try again.');
} finally { } finally {
setProcessingMatchId(null); setProcessingMatchId(null);
setShowCancelModal(false);
setMatchToProcess(null);
} }
}; };
@@ -316,6 +334,38 @@ const DashboardPage = () => {
</section> </section>
)} )}
</div> </div>
{/* Decline Confirmation Modal */}
<ConfirmationModal
isOpen={showDeclineModal}
onClose={() => {
setShowDeclineModal(false);
setMatchToProcess(null);
}}
onConfirm={confirmRejectMatch}
title="Decline Match Request"
message="Are you sure you want to decline this request? This action cannot be undone."
confirmText="Decline"
cancelText="Cancel"
isLoading={processingMatchId === matchToProcess}
loadingText="Declining..."
/>
{/* Cancel Confirmation Modal */}
<ConfirmationModal
isOpen={showCancelModal}
onClose={() => {
setShowCancelModal(false);
setMatchToProcess(null);
}}
onConfirm={confirmCancelRequest}
title="Cancel Match Request"
message="Are you sure you want to cancel this request? This action cannot be undone."
confirmText="Cancel Request"
cancelText="Keep Request"
isLoading={processingMatchId === matchToProcess}
loadingText="Cancelling..."
/>
</Layout> </Layout>
); );
}; };

View File

@@ -8,6 +8,7 @@ import { Loader2, Users } from 'lucide-react';
import { connectSocket, disconnectSocket, getSocket } from '../services/socket'; import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
import { MatchCard } from '../components/matches'; import { MatchCard } from '../components/matches';
import { MATCH_STATUS, MATCH_FILTER } from '../constants'; import { MATCH_STATUS, MATCH_FILTER } from '../constants';
import ConfirmationModal from '../components/modals/ConfirmationModal';
const MatchesPage = () => { const MatchesPage = () => {
const { user } = useAuth(); const { user } = useAuth();
@@ -16,6 +17,8 @@ const MatchesPage = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState(MATCH_FILTER.ALL); const [filter, setFilter] = useState(MATCH_FILTER.ALL);
const [processingMatchId, setProcessingMatchId] = useState(null); const [processingMatchId, setProcessingMatchId] = useState(null);
const [showRejectModal, setShowRejectModal] = useState(false);
const [matchToReject, setMatchToReject] = useState(null);
useEffect(() => { useEffect(() => {
loadMatches(); loadMatches();
@@ -90,22 +93,28 @@ const MatchesPage = () => {
} }
}; };
const handleReject = async (matchSlug) => { const handleReject = (matchSlug) => {
if (!confirm('Are you sure you want to reject this match request?')) { setMatchToReject(matchSlug);
return; setShowRejectModal(true);
} };
const confirmReject = async () => {
if (!matchToReject) return;
try { try {
setProcessingMatchId(matchSlug); setProcessingMatchId(matchToReject);
await matchesAPI.deleteMatch(matchSlug); await matchesAPI.deleteMatch(matchToReject);
// Remove from list // Remove from list
setMatches(prev => prev.filter(m => m.slug !== matchSlug)); setMatches(prev => prev.filter(m => m.slug !== matchToReject));
toast.success('Match request rejected');
} catch (error) { } catch (error) {
console.error('Failed to reject match:', error); console.error('Failed to reject match:', error);
toast.error('Failed to reject match. Please try again.'); toast.error('Failed to reject match. Please try again.');
} finally { } finally {
setProcessingMatchId(null); setProcessingMatchId(null);
setShowRejectModal(false);
setMatchToReject(null);
} }
}; };
@@ -232,6 +241,22 @@ const MatchesPage = () => {
</div> </div>
)} )}
</div> </div>
{/* Reject Confirmation Modal */}
<ConfirmationModal
isOpen={showRejectModal}
onClose={() => {
setShowRejectModal(false);
setMatchToReject(null);
}}
onConfirm={confirmReject}
title="Reject Match Request"
message="Are you sure you want to reject this match request? This action cannot be undone."
confirmText="Reject"
cancelText="Cancel"
isLoading={processingMatchId === matchToReject}
loadingText="Rejecting..."
/>
</Layout> </Layout>
); );
}; };

View File

@@ -1,7 +1,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import toast from 'react-hot-toast';
import { Mail, User, Calendar, Trash2, Check, Eye, AlertCircle, Loader2, Filter } from 'lucide-react'; import { Mail, User, Calendar, Trash2, Check, Eye, AlertCircle, Loader2, Filter } from 'lucide-react';
import Layout from '../../components/layout/Layout'; import Layout from '../../components/layout/Layout';
import ConfirmationModal from '../../components/modals/ConfirmationModal';
// This would normally come from an admin API module, but for now we'll add it inline // This would normally come from an admin API module, but for now we'll add it inline
const adminContactAPI = { const adminContactAPI = {
@@ -73,6 +75,8 @@ export default function ContactMessagesPage() {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
const [selectedMessage, setSelectedMessage] = useState(null); const [selectedMessage, setSelectedMessage] = useState(null);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [messageToDelete, setMessageToDelete] = useState(null);
useEffect(() => { useEffect(() => {
const userData = localStorage.getItem('user'); const userData = localStorage.getItem('user');
@@ -115,19 +119,27 @@ export default function ContactMessagesPage() {
} }
}; };
const handleDelete = async (id) => { const handleDelete = (id) => {
if (!confirm('Are you sure you want to delete this message?')) { setMessageToDelete(id);
return; setShowDeleteModal(true);
} };
const confirmDelete = async () => {
if (!messageToDelete) return;
try { try {
await adminContactAPI.deleteMessage(id); await adminContactAPI.deleteMessage(messageToDelete);
await fetchMessages(); await fetchMessages();
if (selectedMessage?.id === id) { if (selectedMessage?.id === messageToDelete) {
setSelectedMessage(null); setSelectedMessage(null);
} }
toast.success('Message deleted successfully');
} catch (error) { } catch (error) {
console.error('Failed to delete message:', error); console.error('Failed to delete message:', error);
toast.error('Failed to delete message. Please try again.');
} finally {
setShowDeleteModal(false);
setMessageToDelete(null);
} }
}; };
@@ -343,6 +355,20 @@ export default function ContactMessagesPage() {
</div> </div>
</div> </div>
</div> </div>
{/* Delete Confirmation Modal */}
<ConfirmationModal
isOpen={showDeleteModal}
onClose={() => {
setShowDeleteModal(false);
setMessageToDelete(null);
}}
onConfirm={confirmDelete}
title="Delete Message"
message="Are you sure you want to delete this contact message? This action cannot be undone."
confirmText="Delete"
cancelText="Cancel"
/>
</Layout> </Layout>
); );
} }