File size: 5,825 Bytes
ffb4c35 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | import React, { useState } from 'react';
import { motion } from 'framer-motion';
import {
Calendar,
User,
MessageSquare,
Paperclip,
MoreVertical,
CheckCircle,
Clock,
AlertCircle
} from 'lucide-react';
import { tasksAPI } from '../services/api';
import toast from 'react-hot-toast';
const TaskCard = ({ task, onUpdate, onEdit, onDelete }) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const getStatusIcon = (status) => {
switch (status) {
case 'completed':
return <CheckCircle className="w-4 h-4 text-green-500" />;
case 'in-progress':
return <Clock className="w-4 h-4 text-blue-500" />;
case 'pending':
return <AlertCircle className="w-4 h-4 text-yellow-500" />;
default:
return null;
}
};
const getPriorityColor = (priority) => {
switch (priority) {
case 'high':
return 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400';
case 'medium':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400';
case 'low':
return 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400';
}
};
const handleStatusChange = async (newStatus) => {
setIsUpdating(true);
try {
await tasksAPI.updateStatus(task._id, newStatus);
onUpdate && onUpdate(task._id, { status: newStatus });
toast.success('Task status updated');
} catch (error) {
toast.error('Failed to update status');
} finally {
setIsUpdating(false);
}
};
const formatDate = (date) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="card hover:shadow-md transition-shadow duration-200"
>
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{getStatusIcon(task.status)}
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
{task.title}
</h3>
</div>
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getPriorityColor(task.priority)}`}>
{task.priority}
</span>
</div>
<div className="relative">
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<MoreVertical className="w-4 h-4 text-gray-500" />
</button>
{isMenuOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
<button
onClick={() => {
onEdit && onEdit(task);
setIsMenuOpen(false);
}}
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-left hover:bg-gray-50 dark:hover:bg-gray-700"
>
Edit
</button>
<select
value={task.status}
onChange={(e) => {
handleStatusChange(e.target.value);
setIsMenuOpen(false);
}}
className="w-full px-3 py-2 text-sm bg-transparent border-0 focus:ring-0 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
disabled={isUpdating}
>
<option value="pending">Pending</option>
<option value="in-progress">In Progress</option>
<option value="completed">Completed</option>
</select>
<button
onClick={() => {
onDelete && onDelete(task._id);
setIsMenuOpen(false);
}}
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-left text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
>
Delete
</button>
</div>
)}
</div>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
{task.description}
</p>
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-4">
{task.assignee && (
<div className="flex items-center gap-1">
<User className="w-3 h-3" />
<span>{task.assignee.name}</span>
</div>
)}
{task.deadline && (
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
<span>{formatDate(task.deadline)}</span>
</div>
)}
</div>
<div className="flex items-center gap-3">
{task.comments && task.comments.length > 0 && (
<div className="flex items-center gap-1">
<MessageSquare className="w-3 h-3" />
<span>{task.comments.length}</span>
</div>
)}
{task.attachments && task.attachments.length > 0 && (
<div className="flex items-center gap-1">
<Paperclip className="w-3 h-3" />
<span>{task.attachments.length}</span>
</div>
)}
</div>
</div>
</motion.div>
);
};
export default TaskCard; |