Add src/components/NewTaskModal.tsx

This commit is contained in:
2026-05-02 18:03:59 -04:00
parent 63d580ac4f
commit e283a22a8a

View File

@@ -0,0 +1,234 @@
'use client';
import { useApp } from './AppProvider';
import { useState, useEffect, useRef } from 'react';
import { format } from 'date-fns';
export default function NewTaskModal() {
const {
showNewTaskModal, setShowNewTaskModal,
projects, selectedProject, refreshTasks,
} = useApp();
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [projectId, setProjectId] = useState(selectedProject || '');
const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'urgent'>('medium');
const [dueDate, setDueDate] = useState('');
const [status, setStatus] = useState<'todo' | 'in_progress'>('todo');
const [showRecurrence, setShowRecurrence] = useState(false);
const [recurrenceRule, setRecurrenceRule] = useState<'none' | 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'yearly'>('none');
const [recurrenceInterval, setRecurrenceInterval] = useState(1);
const modalRef = useRef<HTMLDivElement>(null);
const titleInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (showNewTaskModal && titleInputRef.current) {
titleInputRef.current.focus();
}
}, [showNewTaskModal]);
useEffect(() => {
if (showNewTaskModal) {
setProjectId(selectedProject || '');
}
}, [showNewTaskModal, selectedProject]);
const handleCreate = async () => {
if (!title.trim()) return;
try {
await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: title.trim(),
description: description.trim(),
projectId: projectId || null,
priority,
dueDate: dueDate || null,
status,
recurrenceRule,
recurrenceInterval,
}),
});
setTitle('');
setDescription('');
setPriority('medium');
setDueDate('');
setStatus('todo');
setShowRecurrence(false);
setRecurrenceRule('none');
setRecurrenceInterval(1);
setShowNewTaskModal(false);
refreshTasks();
} catch (error) {
console.error('Failed to create task:', error);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setShowNewTaskModal(false);
}
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
handleCreate();
}
};
if (!showNewTaskModal) return null;
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) setShowNewTaskModal(false); }}
>
<div
ref={modalRef}
className="bg-card rounded-xl w-full max-w-md mx-4 shadow-xl animate-slide-in"
onKeyDown={handleKeyDown}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border">
<h2 className="text-lg font-bold">New Task</h2>
<button
onClick={() => setShowNewTaskModal(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
ref={titleInputRef}
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Task title"
className="w-full bg-content border border-border rounded-lg px-3 py-2.5 text-sm 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 (optional)"
rows={2}
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"
/>
{/* Project */}
<select
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
className="w-full 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>
{/* Priority & Due Date row */}
<div className="grid grid-cols-2 gap-3">
<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>
{/* Status */}
<select
value={status}
onChange={(e) => setStatus(e.target.value as any)}
className="w-full 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>
</select>
{/* Recurrence toggle */}
<div className="flex items-center justify-between">
<button
onClick={() => setShowRecurrence(!showRecurrence)}
className={`text-sm flex items-center gap-2 ${showRecurrence ? 'text-accent' : 'text-text-secondary'}`}
>
🔄 Recurring task
</button>
</div>
{/* Recurrence options */}
{showRecurrence && (
<div className="space-y-2 animate-slide-in">
<select
value={recurrenceRule}
onChange={(e) => setRecurrenceRule(e.target.value as any)}
className="w-full 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' && (
<div className="flex items-center gap-2">
<span className="text-sm text-text-secondary">Every</span>
<input
type="number"
min={1}
max={365}
value={recurrenceInterval}
onChange={(e) => setRecurrenceInterval(Number(e.target.value))}
className="w-20 bg-content border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-accent"
/>
<span className="text-sm text-text-secondary">
{recurrenceInterval === 1 ? '' : recurrenceInterval + ' '}
{recurrenceRule === 'daily' ? 'day' : recurrenceRule === 'weekly' ? 'week' : recurrenceRule === 'biweekly' ? 'weeks' : recurrenceRule === 'monthly' ? 'month' : recurrenceRule === 'yearly' ? 'year' : ''}
{recurrenceInterval > 1 ? 's' : ''}
</span>
</div>
)}
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-2 p-4 border-t border-border">
<button
onClick={() => setShowNewTaskModal(false)}
className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary transition-colors"
>
Cancel
</button>
<button
onClick={handleCreate}
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"
>
Create Task
</button>
</div>
</div>
</div>
);
}