Add src/components/TaskCard.tsx
This commit is contained in:
148
src/components/TaskCard.tsx
Normal file
148
src/components/TaskCard.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import { useApp, type Task } from './AppProvider';
|
||||
import { format, isToday, isTomorrow, isPast, parseISO } from 'date-fns';
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
projectColor?: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
const priorityConfig = {
|
||||
urgent: { color: 'text-[var(--priority-urgent)]', bg: 'bg-[var(--priority-urgent)]/10', label: 'Urgent', icon: '🔴' },
|
||||
high: { color: 'text-[var(--priority-high)]', bg: 'bg-[var(--priority-high)]/10', label: 'High', icon: '🟠' },
|
||||
medium: { color: 'text-[var(--priority-medium)]', bg: 'bg-[var(--priority-medium)]/10', label: 'Medium', icon: '🟡' },
|
||||
low: { color: 'text-[var(--priority-low)]', bg: 'bg-[var(--priority-low)]/10', label: 'Low', icon: '🟢' },
|
||||
};
|
||||
|
||||
export default function TaskCard({ task, projectColor, projectName }: TaskCardProps) {
|
||||
const { setSelectedTask, setShowEditTaskModal, setEditingTask, refreshTasks } = useApp();
|
||||
|
||||
const handleToggle = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${task.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'toggle-done' }),
|
||||
});
|
||||
if (res.ok) {
|
||||
refreshTasks();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm('Delete this task?')) return;
|
||||
try {
|
||||
await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' });
|
||||
refreshTasks();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
setSelectedTask(task);
|
||||
setEditingTask(task);
|
||||
setShowEditTaskModal(true);
|
||||
};
|
||||
|
||||
const priority = priorityConfig[task.priority];
|
||||
const dueDate = task.dueDate ? parseISO(task.dueDate) : null;
|
||||
|
||||
let dueDateLabel = '';
|
||||
let dueDateClass = '';
|
||||
if (dueDate) {
|
||||
if (isToday(dueDate)) {
|
||||
dueDateLabel = 'Today';
|
||||
dueDateClass = 'text-[var(--priority-urgent)] font-medium';
|
||||
} else if (isTomorrow(dueDate)) {
|
||||
dueDateLabel = 'Tomorrow';
|
||||
dueDateClass = 'text-[var(--priority-high)]';
|
||||
} else if (isPast(dueDate) && !task.completed) {
|
||||
dueDateLabel = format(dueDate, 'MMM d');
|
||||
dueDateClass = 'text-[var(--priority-urgent)] font-medium';
|
||||
} else {
|
||||
dueDateLabel = format(dueDate, 'MMM d');
|
||||
dueDateClass = 'text-text-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
const hasRecurrence = task.recurrenceRule !== 'none';
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={`group flex items-center gap-3 p-3 bg-card border border-border rounded-lg hover:shadow-sm transition-all cursor-pointer ${
|
||||
task.completed ? 'opacity-60' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Checkbox */}
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0 transition-colors ${
|
||||
task.completed
|
||||
? 'bg-accent border-accent'
|
||||
: 'border-text-secondary hover:border-accent'
|
||||
}`}
|
||||
>
|
||||
{task.completed && <span className="text-white text-xs">✓</span>}
|
||||
</button>
|
||||
|
||||
{/* Project indicator */}
|
||||
{projectColor && (
|
||||
<span
|
||||
className="w-2 h-8 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: projectColor }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Task content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-medium truncate ${task.completed ? 'line-through text-text-secondary' : ''}`}>
|
||||
{task.title}
|
||||
</span>
|
||||
{hasRecurrence && <span title="Recurring">🔄</span>}
|
||||
</div>
|
||||
{task.description && (
|
||||
<p className="text-xs text-text-secondary truncate mt-0.5">{task.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Priority badge */}
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${priority.bg} ${priority.color} flex-shrink-0`}>
|
||||
{priority.icon} {priority.label}
|
||||
</span>
|
||||
|
||||
{/* Due date */}
|
||||
{dueDate && (
|
||||
<span className={`text-xs flex-shrink-0 ${dueDateClass}`}>
|
||||
{dueDateLabel}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Status badge */}
|
||||
{task.status !== 'done' && (
|
||||
<span className={`text-xs px-2 py-0.5 rounded flex-shrink-0 ${
|
||||
task.status === 'in_progress' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
{task.status === 'in_progress' ? 'In Progress' : 'Todo'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 transition-all flex-shrink-0"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user