Add src/components/EditTaskModal.tsx

This commit is contained in:
2026-05-02 18:04:01 -04:00
parent e283a22a8a
commit f61d2f032c

View File

@@ -0,0 +1,340 @@
'use client';
import { useApp, type Task } from './AppProvider';
import { useState, useEffect, useRef } from 'react';
import { format } from 'date-fns';
export default function EditTaskModal() {
const {
showEditTaskModal, setShowEditTaskModal,
selectedTask, editingTask, setEditingTask,
projects, refreshTasks,
} = useApp();
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [projectId, setProjectId] = useState('');
const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'urgent'>('medium');
const [dueDate, setDueDate] = useState('');
const [status, setStatus] = useState<'todo' | 'in_progress' | 'done'>('todo');
const [recurrenceRule, setRecurrenceRule] = useState<'none' | 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'yearly'>('none');
const [recurrenceInterval, setRecurrenceInterval] = useState(1);
const [subtasks, setSubtasks] = useState<Task[]>([]);
const [newSubtaskTitle, setNewSubtaskTitle] = useState('');
useEffect(() => {
if (editingTask) {
setTitle(editingTask.title);
setDescription(editingTask.description);
setProjectId(editingTask.projectId || '');
setPriority(editingTask.priority);
setDueDate(editingTask.dueDate ? format(new Date(editingTask.dueDate), 'yyyy-MM-dd') : '');
setStatus(editingTask.status);
setRecurrenceRule(editingTask.recurrenceRule);
setRecurrenceInterval(editingTask.recurrenceInterval);
}
}, [editingTask]);
// Fetch subtasks when modal opens
useEffect(() => {
if (showEditTaskModal && editingTask) {
fetch(`/api/tasks/${editingTask.id}`)
.then(res => res.json())
.then(data => {
setSubtasks(data.subtasks || []);
})
.catch(console.error);
}
}, [showEditTaskModal, editingTask]);
const handleSave = async () => {
if (!editingTask || !title.trim()) return;
try {
await fetch(`/api/tasks/${editingTask.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: title.trim(),
description: description.trim(),
projectId: projectId || null,
priority,
dueDate: dueDate || null,
status,
recurrenceRule,
recurrenceInterval,
}),
});
setShowEditTaskModal(false);
refreshTasks();
} catch (error) {
console.error('Failed to update task:', error);
}
};
const handleDelete = async () => {
if (!editingTask) return;
if (!confirm('Delete this task and all its subtasks?')) return;
try {
await fetch(`/api/tasks/${editingTask.id}`, { method: 'DELETE' });
setShowEditTaskModal(false);
refreshTasks();
} catch (error) {
console.error('Failed to delete task:', error);
}
};
const handleToggleSubtask = async (subtask: Task) => {
try {
await fetch(`/api/tasks/${subtask.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'toggle-done' }),
});
// Refresh subtasks
const res = await fetch(`/api/tasks/${editingTask?.id}`);
const data = await res.json();
setSubtasks(data.subtasks || []);
refreshTasks();
} catch (error) {
console.error('Failed to toggle subtask:', error);
}
};
const handleAddSubtask = async () => {
if (!newSubtaskTitle.trim() || !editingTask) return;
try {
await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: newSubtaskTitle.trim(),
projectId: editingTask.projectId,
priority: editingTask.priority,
parentTaskId: editingTask.id,
}),
});
setNewSubtaskTitle('');
// Refresh subtasks
const res = await fetch(`/api/tasks/${editingTask.id}`);
const data = await res.json();
setSubtasks(data.subtasks || []);
refreshTasks();
} catch (error) {
console.error('Failed to add subtask:', error);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setShowEditTaskModal(false);
}
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
handleSave();
}
};
if (!showEditTaskModal || !editingTask) return null;
const priorityConfig = {
urgent: { label: 'Urgent', icon: '🔴', bg: 'bg-[var(--priority-urgent)]/10', text: 'text-[var(--priority-urgent)]' },
high: { label: 'High', icon: '🟠', bg: 'bg-[var(--priority-high)]/10', text: 'text-[var(--priority-high)]' },
medium: { label: 'Medium', icon: '🟡', bg: 'bg-[var(--priority-medium)]/10', text: 'text-[var(--priority-medium)]' },
low: { label: 'Low', icon: '🟢', bg: 'bg-[var(--priority-low)]/10', text: 'text-[var(--priority-low)]' },
};
const prio = priorityConfig[priority];
return (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 animate-fade-in"
onClick={(e) => { if (e.target === e.currentTarget) setShowEditTaskModal(false); }}
>
<div
className="bg-card rounded-xl w-full max-w-lg mx-4 shadow-xl animate-slide-in max-h-[90vh] overflow-y-auto"
onKeyDown={handleKeyDown}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border sticky top-0 bg-card z-10">
<h2 className="text-lg font-bold">Edit Task</h2>
<button
onClick={() => setShowEditTaskModal(false)}
className="text-text-secondary hover:text-text-primary transition-colors text-xl"
>
</button>
</div>
{/* Body */}
<div className="p-4 space-y-3">
{/* Title */}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full bg-content border border-border rounded-lg px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors"
/>
{/* Description */}
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description"
rows={3}
className="w-full bg-content border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors resize-none"
/>
{/* Grid: Project, Priority, Status, Due Date */}
<div className="grid grid-cols-2 gap-3">
<select
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
className="bg-content border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-accent"
>
<option value="">No Project</option>
{projects.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
<select
value={status}
onChange={(e) => setStatus(e.target.value as any)}
className="bg-content border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-accent"
>
<option value="todo">📋 To Do</option>
<option value="in_progress">🔄 In Progress</option>
<option value="done"> Done</option>
</select>
<select
value={priority}
onChange={(e) => setPriority(e.target.value as any)}
className="bg-content border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-accent"
>
<option value="low">🟢 Low</option>
<option value="medium">🟡 Medium</option>
<option value="high">🟠 High</option>
<option value="urgent">🔴 Urgent</option>
</select>
<input
type="date"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
className="bg-content border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-accent"
/>
</div>
{/* Recurrence */}
<div className="space-y-2">
<label className="text-sm font-medium text-text-secondary">Recurrence</label>
<div className="flex items-center gap-2">
<select
value={recurrenceRule}
onChange={(e) => setRecurrenceRule(e.target.value as any)}
className="flex-1 bg-content border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-accent"
>
<option value="none">Not recurring</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="biweekly">Bi-weekly</option>
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
</select>
{recurrenceRule !== 'none' && (
<input
type="number"
min={1}
max={365}
value={recurrenceInterval}
onChange={(e) => setRecurrenceInterval(Number(e.target.value))}
className="w-16 bg-content border border-border rounded-lg px-3 py-2 text-sm text-center focus:outline-none focus:border-accent"
/>
)}
</div>
{recurrenceRule !== 'none' && (
<p className="text-xs text-text-secondary">
Repeats every {recurrenceInterval} {recurrenceRule === 'daily' ? 'day' : recurrenceRule === 'weekly' ? 'week' : recurrenceRule === 'biweekly' ? 'weeks' : recurrenceRule === 'monthly' ? 'month' : recurrenceRule === 'yearly' ? 'year' : ''}
{recurrenceInterval > 1 ? 's' : ''}
</p>
)}
</div>
{/* Subtasks */}
<div>
<label className="text-sm font-medium text-text-secondary">Subtasks ({subtasks.filter(s => !s.completed).length}/{subtasks.length})</label>
<div className="mt-2 space-y-1">
{subtasks.map(subtask => (
<div
key={subtask.id}
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-colors ${
subtask.completed ? 'opacity-60' : ''
}`}
onClick={() => handleToggleSubtask(subtask)}
>
<span className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 ${
subtask.completed ? 'bg-accent border-accent' : 'border-text-secondary'
}`}>
{subtask.completed && <span className="text-white text-xs"></span>}
</span>
<span className={`text-sm flex-1 ${subtask.completed ? 'line-through text-text-secondary' : ''}`}>
{subtask.title}
</span>
</div>
))}
</div>
{/* Add subtask */}
<div className="flex gap-2 mt-2">
<input
type="text"
value={newSubtaskTitle}
onChange={(e) => setNewSubtaskTitle(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleAddSubtask(); }}
placeholder="Add a subtask..."
className="flex-1 bg-content border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-accent"
/>
<button
onClick={handleAddSubtask}
disabled={!newSubtaskTitle.trim()}
className="px-3 py-2 bg-accent/10 text-accent rounded-lg text-sm font-medium hover:bg-accent/20 transition-colors disabled:opacity-50"
>
+
</button>
</div>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-between p-4 border-t border-border sticky bottom-0 bg-card">
<button
onClick={handleDelete}
className="text-sm text-red-500 hover:text-red-600 transition-colors"
>
Delete Task
</button>
<div className="flex gap-2">
<button
onClick={() => setShowEditTaskModal(false)}
className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={!title.trim()}
className="px-4 py-2 bg-accent hover:bg-accent-hover text-white rounded-lg text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Save Changes
</button>
</div>
</div>
</div>
</div>
);
}