TaskFormModal.tsxā¢5.96 kB
import { useState, useEffect } from 'react';
import { X } from 'lucide-react';
interface Task {
id?: number;
story_id: number;
title: string;
description: string;
status: 'todo' | 'in_progress' | 'review' | 'done' | 'blocked';
assignee?: string;
}
interface Story {
id: number;
title: string;
}
interface TaskFormModalProps {
isOpen: boolean;
onClose: () => void;
onSave: () => void;
task?: Task | null;
projectId: number;
defaultStoryId?: number;
}
export default function TaskFormModal({ isOpen, onClose, onSave, task, projectId, defaultStoryId }: TaskFormModalProps) {
const [formData, setFormData] = useState<Task>({
story_id: defaultStoryId || 0,
title: '',
description: '',
status: 'todo',
assignee: '',
});
const [stories, setStories] = useState<Story[]>([]);
useEffect(() => {
if (isOpen) {
fetchStories();
}
}, [isOpen, projectId]);
useEffect(() => {
if (task) {
setFormData(task);
} else {
setFormData({
story_id: defaultStoryId || 0,
title: '',
description: '',
status: 'todo',
assignee: '',
});
}
}, [task, defaultStoryId]);
const fetchStories = async () => {
try {
const response = await fetch(`/api/stories?project_id=${projectId}`);
const data = await response.json();
setStories(data);
} catch (error) {
console.error('Failed to fetch stories:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const url = task
? `/api/tasks/${task.id}`
: '/api/tasks';
const method = task ? 'PATCH' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.ok) {
onSave();
onClose();
}
} catch (error) {
console.error('Failed to save task:', error);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4">
<div className="flex items-center justify-between p-6 border-b">
<h2 className="text-xl font-semibold">{task ? 'Edit Task' : 'Create Task'}</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<X size={24} />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Story</label>
<select
value={formData.story_id}
onChange={(e) => setFormData({ ...formData, story_id: parseInt(e.target.value) })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
required
>
<option value="">Select a story</option>
{stories.map((story) => (
<option key={story.id} value={story.id}>
{story.title}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
rows={4}
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as Task['status'] })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="todo">To Do</option>
<option value="in_progress">In Progress</option>
<option value="review">Review</option>
<option value="done">Done</option>
<option value="blocked">Blocked</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Assignee</label>
<input
type="text"
value={formData.assignee || ''}
onChange={(e) => setFormData({ ...formData, assignee: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Optional"
/>
</div>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
>
{task ? 'Update' : 'Create'}
</button>
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 font-medium"
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
}