From 87b0079b847eb252123f708dd18423d9253d7b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Fri, 21 Nov 2025 21:27:03 +0100 Subject: [PATCH] feat(frontend): add toast notifications for dashboard actions - Install react-hot-toast library - Add Toaster component to App.jsx - Show success/error toasts for match accept/reject/cancel - Show toasts for real-time match events - Update tests with toast mocks --- frontend/package-lock.json | 28 +++++++++++++++- frontend/package.json | 1 + frontend/src/App.jsx | 25 +++++++++++++++ frontend/src/pages/DashboardPage.jsx | 32 +++++++++++++++---- .../pages/__tests__/DashboardPage.test.jsx | 14 +++++--- 5 files changed, 88 insertions(+), 12 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 96872e9..bd6fcd8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "qrcode.react": "^4.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.9.5", "socket.io-client": "^4.8.1" }, @@ -4168,7 +4169,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/data-urls": { @@ -5402,6 +5402,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -7274,6 +7283,23 @@ "react": "^19.2.0" } }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index a967fa2..3749306 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "qrcode.react": "^4.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.9.5", "socket.io-client": "^4.8.1" }, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8436500..85c37f5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,5 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { Toaster } from 'react-hot-toast'; import { AuthProvider, useAuth } from './contexts/AuthContext'; import HomePage from './pages/HomePage'; import LoginPage from './pages/LoginPage'; @@ -70,6 +71,30 @@ function App() { {/* PWA Install Prompt */} + {/* Toast Notifications */} + + {/* Public Routes */} { const socket = getSocket(); if (socket) { - socket.on('match_request_received', handleRealtimeUpdate); - socket.on('match_accepted', handleRealtimeUpdate); + socket.on('match_request_received', handleMatchRequest); + socket.on('match_accepted', handleMatchAccepted); socket.on('match_cancelled', handleRealtimeUpdate); socket.on('new_message', handleRealtimeUpdate); } @@ -49,8 +50,8 @@ const DashboardPage = () => { return () => { const socket = getSocket(); if (socket) { - socket.off('match_request_received', handleRealtimeUpdate); - socket.off('match_accepted', handleRealtimeUpdate); + socket.off('match_request_received', handleMatchRequest); + socket.off('match_accepted', handleMatchAccepted); socket.off('match_cancelled', handleRealtimeUpdate); socket.off('new_message', handleRealtimeUpdate); } @@ -58,6 +59,20 @@ const DashboardPage = () => { }; }, [user]); + const handleMatchRequest = (data) => { + toast.success(`New match request from ${data.requesterUsername || 'someone'}!`, { + icon: '📨', + }); + loadDashboard(); + }; + + const handleMatchAccepted = (data) => { + toast.success('Match accepted! You can now chat.', { + icon: '🎉', + }); + loadDashboard(); + }; + const loadDashboard = async () => { try { setLoading(true); @@ -79,10 +94,11 @@ const DashboardPage = () => { try { setProcessingMatchId(matchSlug); await matchesAPI.acceptMatch(matchSlug); + toast.success('Match accepted! You can now chat.', { icon: '🎉' }); await loadDashboard(); } catch (err) { console.error('Failed to accept match:', err); - alert('Failed to accept match. Please try again.'); + toast.error('Failed to accept match. Please try again.'); } finally { setProcessingMatchId(null); } @@ -94,10 +110,11 @@ const DashboardPage = () => { try { setProcessingMatchId(matchSlug); await matchesAPI.deleteMatch(matchSlug); + toast.success('Request declined.'); await loadDashboard(); } catch (err) { console.error('Failed to reject match:', err); - alert('Failed to decline request. Please try again.'); + toast.error('Failed to decline request. Please try again.'); } finally { setProcessingMatchId(null); } @@ -109,10 +126,11 @@ const DashboardPage = () => { try { setProcessingMatchId(matchSlug); await matchesAPI.deleteMatch(matchSlug); + toast.success('Request cancelled.'); await loadDashboard(); } catch (err) { console.error('Failed to cancel request:', err); - alert('Failed to cancel request. Please try again.'); + toast.error('Failed to cancel request. Please try again.'); } finally { setProcessingMatchId(null); } diff --git a/frontend/src/pages/__tests__/DashboardPage.test.jsx b/frontend/src/pages/__tests__/DashboardPage.test.jsx index e694105..d0dadaa 100644 --- a/frontend/src/pages/__tests__/DashboardPage.test.jsx +++ b/frontend/src/pages/__tests__/DashboardPage.test.jsx @@ -4,6 +4,13 @@ import '@testing-library/jest-dom'; import { BrowserRouter } from 'react-router-dom'; // Mock the modules +vi.mock('react-hot-toast', () => ({ + default: { + success: vi.fn(), + error: vi.fn(), + }, +})); + vi.mock('../../contexts/AuthContext', () => ({ useAuth: () => ({ user: { id: 1, username: 'testuser', firstName: 'Test' }, @@ -407,10 +414,9 @@ describe('DashboardPage', () => { expect(screen.getByText('John Lead')).toBeInTheDocument(); }); - // Find the accept button (green button with checkmark) - const acceptButtons = screen.getAllByRole('button'); - const acceptButton = acceptButtons.find(btn => btn.title === 'Accept'); - expect(acceptButton).toBeInTheDocument(); + // Find the accept button by title attribute + const acceptButton = document.querySelector('button[title="Accept"]'); + expect(acceptButton).toBeTruthy(); fireEvent.click(acceptButton);