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