Add src/components/TaskCard.tsx

This commit is contained in:
2026-05-02 18:03:51 -04:00
parent b02e9cb0ce
commit 6ac7d2a30a

148
src/components/TaskCard.tsx Normal file
View 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>
);
}