Add src/components/EditTaskModal.tsx
This commit is contained in:
340
src/components/EditTaskModal.tsx
Normal file
340
src/components/EditTaskModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user