From 4de72fe18f164da31ed73f3f4f0269f6d197dc2a Mon Sep 17 00:00:00 2001 From: Chirag8405 Date: Tue, 28 Oct 2025 09:52:20 +0530 Subject: [PATCH] Implement toast notifications for task and project actions; add ToastContainer component --- client/src/App.jsx | 2 + .../src/components/board/TaskCreateForm.jsx | 9 +- .../components/common/NotificationPanel.jsx | 12 +-- .../src/components/common/ToastContainer.jsx | 87 +++++++++++++++++++ .../src/components/dashboard/QuickActions.jsx | 14 +-- client/src/components/tasks/TaskModal.jsx | 14 +-- client/src/hooks/useToast.js | 57 ++++++++++++ client/src/pages/ProjectBoard.jsx | 7 +- client/src/services/notificationService.js | 2 - 9 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 client/src/components/common/ToastContainer.jsx create mode 100644 client/src/hooks/useToast.js diff --git a/client/src/App.jsx b/client/src/App.jsx index 1abebd7..2237ee4 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -4,6 +4,7 @@ import { AuthProvider } from './context/AuthContext'; import { SocketProvider } from './context/SocketContext'; import { NotificationProvider } from './context/NotificationContext'; import { ErrorBoundary } from './components/common'; +import ToastContainer from './components/common/ToastContainer'; import ProtectedRoute from './components/auth/ProtectedRoute'; import LoginForm from './components/auth/LoginForm'; import RegisterForm from './components/auth/RegisterForm'; @@ -91,6 +92,7 @@ function App() { } /> + diff --git a/client/src/components/board/TaskCreateForm.jsx b/client/src/components/board/TaskCreateForm.jsx index 3963667..ddb792f 100644 --- a/client/src/components/board/TaskCreateForm.jsx +++ b/client/src/components/board/TaskCreateForm.jsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { Button, Input, Textarea, Select } from '../common'; +import { useToast } from '../../hooks/useToast'; import { Calendar, Flag, User } from 'lucide-react'; const TaskCreateForm = ({ onSubmit, onCancel }) => { @@ -11,6 +12,7 @@ const TaskCreateForm = ({ onSubmit, onCancel }) => { assignee: '' }); const [loading, setLoading] = useState(false); + const { toast } = useToast(); const priorityOptions = [ { value: 'low', label: 'Low Priority', color: 'text-green-600' }, @@ -31,10 +33,12 @@ const TaskCreateForm = ({ onSubmit, onCancel }) => { ...formData, title: formData.title.trim(), description: formData.description.trim(), - dueDate: formData.dueDate || null + dueDate: formData.dueDate || null, + assignee: formData.assignee || null }; await onSubmit(taskData); + toast.success('Task created successfully'); // Reset form setFormData({ @@ -45,7 +49,8 @@ const TaskCreateForm = ({ onSubmit, onCancel }) => { assignee: '' }); } catch (error) { - console.error('Error creating task:', error); + const errorMessage = error.response?.data?.message || error.message || 'Failed to create task'; + toast.error(errorMessage); } finally { setLoading(false); } diff --git a/client/src/components/common/NotificationPanel.jsx b/client/src/components/common/NotificationPanel.jsx index 374b35b..09035dc 100644 --- a/client/src/components/common/NotificationPanel.jsx +++ b/client/src/components/common/NotificationPanel.jsx @@ -31,13 +31,13 @@ const NotificationItem = ({ notification, onRemove, onMarkAsRead }) => { onClick={() => !notification.read && onMarkAsRead(notification.id)} >
-
+
-

+

{notification.title}

-

+

{notification.message}

@@ -50,7 +50,7 @@ const NotificationItem = ({ notification, onRemove, onMarkAsRead }) => { e.stopPropagation(); onRemove(notification.id); }} - className="ml-3 text-gray-400 hover:text-gray-600 transition-colors" + className="ml-3 text-gray-400 hover:text-gray-600 transition-colors flex-shrink-0" > @@ -98,14 +98,14 @@ const NotificationPanel = ({ isOpen, onToggle }) => { )} -

+

Notifications

{notifications.length > 0 && ( diff --git a/client/src/components/common/ToastContainer.jsx b/client/src/components/common/ToastContainer.jsx new file mode 100644 index 0000000..27a0d8f --- /dev/null +++ b/client/src/components/common/ToastContainer.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { useNotifications } from '../../context/NotificationContext'; +import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react'; + +const ToastIcon = ({ type }) => { + switch (type) { + case 'success': + return ; + case 'error': + return ; + case 'warning': + return ; + default: + return ; + } +}; + +const ToastNotification = ({ notification, onRemove }) => { + const typeStyles = { + success: 'border-green-200 bg-green-50 text-green-800', + error: 'border-red-200 bg-red-50 text-red-800', + warning: 'border-yellow-200 bg-yellow-50 text-yellow-800', + info: 'border-blue-200 bg-blue-50 text-blue-800' + }; + + return ( +
+
+
+
+ +
+
+ {notification.title && ( +

+ {notification.title} +

+ )} +

+ {notification.message} +

+
+
+
+
+ +
+
+ ); +}; + +const ToastContainer = () => { + const { notifications, removeNotification } = useNotifications(); + + // Filter notifications that should appear as toasts (temporary ones) + const toastNotifications = notifications.filter(n => n.isToast); + + if (toastNotifications.length === 0) return null; + + return ( +
+
+ {toastNotifications.map((notification) => ( + + ))} +
+
+ ); +}; + +export default ToastContainer; \ No newline at end of file diff --git a/client/src/components/dashboard/QuickActions.jsx b/client/src/components/dashboard/QuickActions.jsx index a29c4c7..0ed70ba 100644 --- a/client/src/components/dashboard/QuickActions.jsx +++ b/client/src/components/dashboard/QuickActions.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { Button } from '../common'; +import { useToast } from '../../hooks/useToast'; import { Plus, Calendar, @@ -10,6 +11,7 @@ import { } from 'lucide-react'; const QuickActions = ({ onAction, className = '' }) => { + const { toast } = useToast(); const actions = [ { id: 'create-task', @@ -75,19 +77,19 @@ const QuickActions = ({ onAction, className = '' }) => { switch (actionId) { case 'create-task': // Navigate to task creation or open modal - console.log('Create task clicked'); + toast.info('Task creation feature coming soon'); break; case 'create-project': // Navigate to project creation - console.log('Create project clicked'); + toast.info('Project creation feature coming soon'); break; - case 'schedule-meeting': + case 'schedule-meeting': // Open calendar scheduling - console.log('Schedule meeting clicked'); + toast.info('Meeting scheduling feature coming soon'); break; case 'invite-member': // Open invite modal - console.log('Invite member clicked'); + toast.info('Member invitation feature coming soon'); break; case 'view-calendar': // Navigate to calendar @@ -98,7 +100,7 @@ const QuickActions = ({ onAction, className = '' }) => { window.location.href = '/tasks'; break; default: - console.log('Unknown action:', actionId); + toast.warning('Unknown action requested'); } } }; diff --git a/client/src/components/tasks/TaskModal.jsx b/client/src/components/tasks/TaskModal.jsx index da94881..bd7bf2b 100644 --- a/client/src/components/tasks/TaskModal.jsx +++ b/client/src/components/tasks/TaskModal.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { taskService } from '../../services/taskService'; import { userService } from '../../services/userService'; import { projectService } from '../../services/projectService'; +import { useToast } from '../../hooks/useToast'; import { X, Calendar, User, Flag, FileText, FolderOpen } from 'lucide-react'; const TaskModal = ({ isOpen, onClose, task, projectId, onSave }) => { @@ -18,6 +19,7 @@ const TaskModal = ({ isOpen, onClose, task, projectId, onSave }) => { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); const [errors, setErrors] = useState({}); + const { toast } = useToast(); useEffect(() => { if (isOpen) { @@ -56,6 +58,7 @@ const TaskModal = ({ isOpen, onClose, task, projectId, onSave }) => { setUsers(Array.isArray(usersData) ? usersData : []); } catch (error) { console.error('Error fetching users:', error); + toast.error('Failed to load users'); setUsers([]); } }; @@ -67,7 +70,7 @@ const TaskModal = ({ isOpen, onClose, task, projectId, onSave }) => { const projectsData = response.data?.projects || response.projects || response.data || []; setProjects(Array.isArray(projectsData) ? projectsData : []); } catch (error) { - console.error('Error fetching projects:', error); + toast.error('Failed to load projects'); setProjects([]); } }; @@ -129,18 +132,19 @@ const TaskModal = ({ isOpen, onClose, task, projectId, onSave }) => { let response; if (task) { response = await taskService.updateTask(task._id, submitData); + toast.success('Task updated successfully'); } else { response = await taskService.createTask(submitData); + toast.success('Task created successfully'); } onSave(response.data); onClose(); } catch (error) { - console.error('Error saving task:', error); - console.error('Error response:', error.response?.data); - console.error('Submitted data:', submitData); + const errorMessage = error.response?.data?.message || 'Failed to save task'; + toast.error(errorMessage); setErrors({ - submit: error.response?.data?.message || 'Failed to save task' + submit: errorMessage }); } finally { setLoading(false); diff --git a/client/src/hooks/useToast.js b/client/src/hooks/useToast.js new file mode 100644 index 0000000..415935a --- /dev/null +++ b/client/src/hooks/useToast.js @@ -0,0 +1,57 @@ +import { useNotifications } from '../context/NotificationContext'; + +export const useToast = () => { + const { addNotification } = useNotifications(); + + const toast = { + success: (message, title = '') => { + addNotification({ + id: Date.now() + Math.random(), + type: 'success', + title, + message, + timestamp: new Date(), + isToast: true, + autoClose: true + }); + }, + + error: (message, title = '') => { + addNotification({ + id: Date.now() + Math.random(), + type: 'error', + title, + message, + timestamp: new Date(), + isToast: true, + autoClose: true + }); + }, + + warning: (message, title = '') => { + addNotification({ + id: Date.now() + Math.random(), + type: 'warning', + title, + message, + timestamp: new Date(), + isToast: true, + autoClose: true + }); + }, + + info: (message, title = '') => { + addNotification({ + id: Date.now() + Math.random(), + type: 'info', + title, + message, + timestamp: new Date(), + isToast: true, + autoClose: true + }); + } + }; + + return { toast }; +}; \ No newline at end of file diff --git a/client/src/pages/ProjectBoard.jsx b/client/src/pages/ProjectBoard.jsx index c1081fd..4df26f3 100644 --- a/client/src/pages/ProjectBoard.jsx +++ b/client/src/pages/ProjectBoard.jsx @@ -4,6 +4,7 @@ import { projectService } from '../services/projectService'; import Layout from '../components/layout/Layout'; import { KanbanBoard } from '../components/board'; import { LoadingSpinner, Button } from '../components/common'; +import { useToast } from '../hooks/useToast'; import { ArrowLeft, Settings, @@ -20,6 +21,7 @@ const ProjectBoard = () => { const { projectId } = useParams(); const [project, setProject] = useState(null); const [loading, setLoading] = useState(true); + const { toast } = useToast(); const [showFilters, setShowFilters] = useState(false); useEffect(() => { @@ -32,14 +34,15 @@ const ProjectBoard = () => { const projectResponse = await projectService.getProject(projectId); setProject(projectResponse.data); } catch (error) { - console.error('Error fetching project:', error); + const errorMessage = error.response?.data?.message || 'Failed to fetch project details'; + toast.error(errorMessage); } finally { setLoading(false); } }; const handleTaskUpdate = (task) => { - console.log('Task updated:', task); + toast.success('Task updated successfully'); }; const getStatusColor = (status) => { diff --git a/client/src/services/notificationService.js b/client/src/services/notificationService.js index 19f9fc0..cc94e4c 100644 --- a/client/src/services/notificationService.js +++ b/client/src/services/notificationService.js @@ -109,12 +109,10 @@ class NotificationService { subscribeToNotifications(callback) { // This would be used with Socket.io context // The actual implementation is in the NotificationPanel component - console.log('Subscribe to notifications:', callback); } unsubscribeFromNotifications(callback) { // This would be used with Socket.io context - console.log('Unsubscribe from notifications:', callback); } }