Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -91,6 +92,7 @@ function App() {
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
</div>
<ToastContainer />
</Router>
</NotificationProvider>
</SocketProvider>
Expand Down
9 changes: 7 additions & 2 deletions client/src/components/board/TaskCreateForm.jsx
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand All @@ -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' },
Expand All @@ -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({
Expand All @@ -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);
}
Expand Down
12 changes: 6 additions & 6 deletions client/src/components/common/NotificationPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ const NotificationItem = ({ notification, onRemove, onMarkAsRead }) => {
onClick={() => !notification.read && onMarkAsRead(notification.id)}
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
<div className="flex items-start space-x-3 min-w-0 flex-1">
<NotificationIcon type={notification.type} />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900">
<p className="text-sm font-medium text-gray-900 break-words">
{notification.title}
</p>
<p className="text-sm text-gray-600 mt-1">
<p className="text-sm text-gray-600 mt-1 break-words">
{notification.message}
</p>
<p className="text-xs text-gray-500 mt-1">
Expand All @@ -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"
>
<X className="h-4 w-4" />
</button>
Expand Down Expand Up @@ -98,14 +98,14 @@ const NotificationPanel = ({ isOpen, onToggle }) => {
)}
</button>

<div className="absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-lg border border-gray-200 z-50 max-h-96 overflow-hidden">
<div className="absolute right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] sm:max-w-80 bg-white rounded-lg shadow-lg border border-gray-200 z-50 max-h-96 overflow-hidden">
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">Notifications</h3>
{notifications.length > 0 && (
<button
onClick={clearAllNotifications}
className="text-sm text-gray-500 hover:text-gray-700 transition-colors"
className="text-sm text-gray-500 hover:text-gray-700 transition-colors whitespace-nowrap"
>
Clear all
</button>
Expand Down
87 changes: 87 additions & 0 deletions client/src/components/common/ToastContainer.jsx
Original file line number Diff line number Diff line change
@@ -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 <CheckCircle className="h-5 w-5 text-green-500" />;
case 'error':
return <AlertCircle className="h-5 w-5 text-red-500" />;
case 'warning':
return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
default:
return <Info className="h-5 w-5 text-blue-500" />;
}
};

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 (
<div
className={`max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5 ${
typeStyles[notification.type] || typeStyles.info
} border`}
>
<div className="flex-1 w-0 p-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<ToastIcon type={notification.type} />
</div>
<div className="ml-3 flex-1">
{notification.title && (
<p className="text-sm font-medium">
{notification.title}
</p>
)}
<p className={`text-sm ${notification.title ? 'mt-1' : ''} opacity-90`}>
{notification.message}
</p>
</div>
</div>
</div>
<div className="flex border-l border-gray-200">
<button
onClick={() => onRemove(notification.id)}
className="w-full border border-transparent rounded-none rounded-r-lg p-4 flex items-center justify-center text-sm font-medium text-gray-600 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
);
};

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 (
<div
aria-live="assertive"
className="fixed inset-0 flex items-end justify-center px-4 py-6 pointer-events-none sm:p-6 sm:items-start sm:justify-end z-50"
>
<div className="w-full flex flex-col items-center space-y-4 sm:items-end">
{toastNotifications.map((notification) => (
<ToastNotification
key={notification.id}
notification={notification}
onRemove={removeNotification}
/>
))}
</div>
</div>
);
};

export default ToastContainer;
14 changes: 8 additions & 6 deletions client/src/components/dashboard/QuickActions.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { Button } from '../common';
import { useToast } from '../../hooks/useToast';
import {
Plus,
Calendar,
Expand All @@ -10,6 +11,7 @@ import {
} from 'lucide-react';

const QuickActions = ({ onAction, className = '' }) => {
const { toast } = useToast();
const actions = [
{
id: 'create-task',
Expand Down Expand Up @@ -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
Expand All @@ -98,7 +100,7 @@ const QuickActions = ({ onAction, className = '' }) => {
window.location.href = '/tasks';
break;
default:
console.log('Unknown action:', actionId);
toast.warning('Unknown action requested');
}
}
};
Expand Down
14 changes: 9 additions & 5 deletions client/src/components/tasks/TaskModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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 }) => {
Expand All @@ -18,6 +19,7 @@
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({});
const { toast } = useToast();

useEffect(() => {
if (isOpen) {
Expand Down Expand Up @@ -46,7 +48,7 @@
}
setErrors({});
}
}, [isOpen, task, projectId]);

Check warning on line 51 in client/src/components/tasks/TaskModal.jsx

View workflow job for this annotation

GitHub Actions / test-frontend (18.x)

React Hook useEffect has missing dependencies: 'fetchProjects' and 'fetchUsers'. Either include them or remove the dependency array

const fetchUsers = async () => {
try {
Expand All @@ -56,6 +58,7 @@
setUsers(Array.isArray(usersData) ? usersData : []);
} catch (error) {
console.error('Error fetching users:', error);
toast.error('Failed to load users');
setUsers([]);
}
};
Expand All @@ -67,7 +70,7 @@
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([]);
}
};
Expand Down Expand Up @@ -129,18 +132,19 @@
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);
Expand Down
57 changes: 57 additions & 0 deletions client/src/hooks/useToast.js
Original file line number Diff line number Diff line change
@@ -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 };
};
7 changes: 5 additions & 2 deletions client/src/pages/ProjectBoard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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,
Expand All @@ -20,11 +21,12 @@
const { projectId } = useParams();
const [project, setProject] = useState(null);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
const [showFilters, setShowFilters] = useState(false);

useEffect(() => {
fetchProject();
}, [projectId]);

Check warning on line 29 in client/src/pages/ProjectBoard.jsx

View workflow job for this annotation

GitHub Actions / test-frontend (18.x)

React Hook useEffect has a missing dependency: 'fetchProject'. Either include it or remove the dependency array

const fetchProject = async () => {
try {
Expand All @@ -32,14 +34,15 @@
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) => {
Expand Down
2 changes: 0 additions & 2 deletions client/src/services/notificationService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
Loading