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
81 changes: 81 additions & 0 deletions client/src/components/common/ConfirmDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import { AlertTriangle, X } from 'lucide-react';

const ConfirmDialog = ({ isOpen, onClose, onConfirm, title, message, confirmText = 'Confirm', cancelText = 'Cancel', type = 'warning' }) => {
if (!isOpen) return null;

const typeStyles = {
warning: {
icon: AlertTriangle,
iconColor: 'text-yellow-600',
confirmButton: 'bg-yellow-600 hover:bg-yellow-700 text-white'
},
danger: {
icon: AlertTriangle,
iconColor: 'text-red-600',
confirmButton: 'bg-red-600 hover:bg-red-700 text-white'
}
};

const config = typeStyles[type] || typeStyles.warning;
const Icon = config.icon;

return (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
{/* Background overlay */}
<div
className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"
onClick={onClose}
/>

{/* Modal */}
<div className="inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-lg sm:align-middle relative">
{/* Close button */}
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
>
<X className="h-5 w-5" />
</button>

{/* Content */}
<div className="flex items-start">
<div className={`flex-shrink-0 ${config.iconColor}`}>
<Icon className="h-6 w-6" />
</div>
<div className="ml-3 w-full">
<h3 className="text-lg font-medium text-gray-900 mb-2">
{title}
</h3>
<p className="text-sm text-gray-600 mb-6">
{message}
</p>

{/* Actions */}
<div className="flex justify-end space-x-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
{cancelText}
</button>
<button
onClick={() => {
onConfirm();
onClose();
}}
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 ${config.confirmButton}`}
>
{confirmText}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

export default ConfirmDialog;
164 changes: 73 additions & 91 deletions client/src/pages/Tasks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import Layout from '../components/layout/Layout';
import { LoadingSpinner, Button, Select, Input } from '../components/common';
import TaskModal from '../components/tasks/TaskModal';
import ConfirmDialog from '../components/common/ConfirmDialog';
import { useToast } from '../hooks/useToast';
import {
Plus,
Search,
Expand Down Expand Up @@ -33,7 +35,10 @@
const [openMenuId, setOpenMenuId] = useState(null);
const [showTaskModal, setShowTaskModal] = useState(false);
const [selectedTask, setSelectedTask] = useState(null);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [taskToDelete, setTaskToDelete] = useState(null);
const menuRef = useRef(null);
const { toast } = useToast();

// Set search query from URL parameter
useEffect(() => {
Expand All @@ -45,11 +50,13 @@

useEffect(() => {
fetchData();
}, []);

Check warning on line 53 in client/src/pages/Tasks.jsx

View workflow job for this annotation

GitHub Actions / test-frontend (18.x)

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



useEffect(() => {
filterTasks();
}, [tasks, searchQuery, statusFilter, priorityFilter, projectFilter]);

Check warning on line 59 in client/src/pages/Tasks.jsx

View workflow job for this annotation

GitHub Actions / test-frontend (18.x)

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

// Close menu when clicking outside
useEffect(() => {
Expand Down Expand Up @@ -79,7 +86,8 @@
setProjects(projectsResponse.data.projects || []);
setTasks(recentTasksResponse.data || []);
} catch (error) {
console.error('Error fetching data:', error);
const errorMessage = error.response?.data?.message || 'Failed to load tasks';
toast.error(errorMessage);
} finally {
setLoading(false);
}
Expand Down Expand Up @@ -114,21 +122,34 @@
setFilteredTasks(filtered);
};

const handleDeleteTask = async (taskId) => {
if (!window.confirm('Are you sure you want to delete this task?')) {
return;
}
const handleDeleteTask = (taskId) => {
const task = tasks.find(t => t._id === taskId);
setTaskToDelete(task);
setShowConfirmDialog(true);
setOpenMenuId(null);
};

const confirmDeleteTask = async () => {
if (!taskToDelete) return;

try {
await taskService.deleteTask(taskId);
setTasks(tasks.filter(task => task._id !== taskId));
setOpenMenuId(null);
await taskService.deleteTask(taskToDelete._id);
setTasks(tasks.filter(task => task._id !== taskToDelete._id));
toast.success(`"${taskToDelete.title}" deleted successfully`);
} catch (error) {
console.error('Error deleting task:', error);
alert('Failed to delete task. Please try again.');
const errorMessage = error.response?.data?.message || 'Failed to delete task';
toast.error(errorMessage);
} finally {
setShowConfirmDialog(false);
setTaskToDelete(null);
}
};

const cancelDeleteTask = () => {
setShowConfirmDialog(false);
setTaskToDelete(null);
};

const handleEditTask = (taskId) => {
const task = tasks.find(t => t._id === taskId);
setSelectedTask(task);
Expand All @@ -155,8 +176,7 @@
setShowTaskModal(false);
fetchData(); // Refresh to get updated task with populated fields
} catch (error) {
console.error('Error saving task:', error);
throw error;
throw error; // Let TaskModal handle the error display
}
};

Expand Down Expand Up @@ -457,52 +477,23 @@
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="relative inline-block" ref={openMenuId === task._id ? menuRef : null}>
<div className="flex gap-2 justify-end">
<button
onClick={(e) => {
e.stopPropagation();
setOpenMenuId(openMenuId === task._id ? null : task._id);
}}
className="text-gray-400 hover:text-gray-600 p-1 rounded hover:bg-gray-100"
id={`menu-button-${task._id}`}
onClick={() => handleEditTask(task._id)}
className="inline-flex items-center px-3 py-1 text-xs font-medium text-blue-700 bg-blue-50 border border-blue-200 rounded-md hover:bg-blue-100 hover:border-blue-300 transition-colors"
title="Edit task"
>
<MoreVertical className="h-4 w-4" />
<Edit className="h-3 w-3 mr-1" />
Edit
</button>
<button
onClick={() => handleDeleteTask(task._id)}
className="inline-flex items-center px-3 py-1 text-xs font-medium text-red-700 bg-red-50 border border-red-200 rounded-md hover:bg-red-100 hover:border-red-300 transition-colors"
title="Delete task"
>
<Trash2 className="h-3 w-3 mr-1" />
Delete
</button>

{openMenuId === task._id && (() => {
const button = document.getElementById(`menu-button-${task._id}`);
const rect = button?.getBoundingClientRect();
return (
<div
className="fixed w-32 bg-white rounded-md shadow-lg border border-gray-200 py-1 z-[9999]"
style={{
top: rect ? `${rect.top - 80}px` : '0px',
left: rect ? `${rect.left - 100}px` : '0px',
}}
>
<button
onClick={(e) => {
e.stopPropagation();
handleEditTask(task._id);
}}
className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"
>
<Edit className="h-3 w-3 mr-2" />
Edit
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteTask(task._id);
}}
className="w-full text-left px-3 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center"
>
<Trash2 className="h-3 w-3 mr-2" />
Delete
</button>
</div>
);
})()}
</div>
</td>
</tr>
Expand All @@ -515,52 +506,32 @@
<div className="md:hidden divide-y divide-gray-200">
{filteredTasks.map((task) => (
<div key={task._id} className="p-4 hover:bg-gray-50">
<div className="flex items-start justify-between mb-2">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center flex-1 min-w-0">
{getStatusIcon(task.status)}
<h3 className="ml-2 text-sm font-medium text-gray-900 truncate">
{task.title}
</h3>
</div>
<div className="relative ml-2 flex-shrink-0" ref={openMenuId === task._id ? menuRef : null}>
<div className="flex gap-1 ml-2 flex-shrink-0">
<button
onClick={(e) => {
e.stopPropagation();
setOpenMenuId(openMenuId === task._id ? null : task._id);
}}
className="text-gray-400 hover:text-gray-600 p-1 rounded hover:bg-gray-100"
onClick={() => handleEditTask(task._id)}
className="inline-flex items-center px-2 py-1 text-xs font-medium text-blue-700 bg-blue-50 border border-blue-200 rounded hover:bg-blue-100 transition-colors"
title="Edit task"
>
<MoreVertical className="h-4 w-4" />
<Edit className="h-3 w-3 mr-1" />
Edit
</button>
<button
onClick={() => handleDeleteTask(task._id)}
className="inline-flex items-center px-2 py-1 text-xs font-medium text-red-700 bg-red-50 border border-red-200 rounded hover:bg-red-100 transition-colors"
title="Delete task"
>
<Trash2 className="h-3 w-3 mr-1" />
Delete
</button>

{openMenuId === task._id && (
<div className="absolute right-0 mt-1 w-32 bg-white rounded-md shadow-lg border border-gray-200 py-1 z-50">
<button
onClick={(e) => {
e.stopPropagation();
handleEditTask(task._id);
}}
className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"
>
<Edit className="h-3 w-3 mr-2" />
Edit
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteTask(task._id);
}}
className="w-full text-left px-3 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center"
>
<Trash2 className="h-3 w-3 mr-2" />
Delete
</button>
</div>
)}
</div>
</div>

{task.description && (
</div> {task.description && (
<p className="text-sm text-gray-500 mb-3 line-clamp-2">
{task.description}
</p>
Expand Down Expand Up @@ -633,6 +604,17 @@
projectId={projectFilter !== 'all' ? projectFilter : null}
onSave={handleSaveTask}
/>

{/* Delete Confirmation Dialog */}
<ConfirmDialog
isOpen={showConfirmDialog}
onClose={cancelDeleteTask}
onConfirm={confirmDeleteTask}
title="Delete Task"
message={`Are you sure you want to delete "${taskToDelete?.title}"? This action cannot be undone.`}
confirmText="Delete"
type="danger"
/>
</Layout>
);
};
Expand Down
Loading