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