diff options
Diffstat (limited to 'src/components/TaskManager.tsx')
| -rw-r--r-- | src/components/TaskManager.tsx | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/src/components/TaskManager.tsx b/src/components/TaskManager.tsx new file mode 100644 index 0000000..a10a5f9 --- /dev/null +++ b/src/components/TaskManager.tsx @@ -0,0 +1,202 @@ +import { useState, useEffect, useCallback } from 'react'; + +interface Task { + id: number; + title: string; + description: string; + completed: boolean; + created_at?: string; + updated_at?: string; +} + +export default function TaskManager() { + const [tasks, setTasks] = useState<Task[]>([]); + const [newTask, setNewTask] = useState({ title: '', description: '' }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + const fetchTasks = useCallback(async () => { + try { + setError(null); + const response = await fetch('/api/tasks'); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log('Fetched tasks:', data); + setTasks(Array.isArray(data) ? data : []); + } catch (error) { + console.error('Failed to fetch tasks:', error); + setError(error instanceof Error ? error.message : 'Failed to fetch tasks'); + setTasks([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchTasks(); + }, [fetchTasks]); + + const createTask = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newTask.title.trim()) return; + + try { + const response = await fetch('/api/tasks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newTask) + }); + + if (response.ok) { + setNewTask({ title: '', description: '' }); + fetchTasks(); + } + } catch (error) { + console.error('Failed to create task:', error); + } + }; + + const toggleTask = async (task: Task) => { + try { + await fetch(`/api/tasks/${task.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...task, completed: !task.completed }) + }); + fetchTasks(); + } catch (error) { + console.error('Failed to update task:', error); + } + }; + + const deleteTask = async (id: number) => { + try { + await fetch(`/api/tasks/${id}`, { method: 'DELETE' }); + fetchTasks(); + } catch (error) { + console.error('Failed to delete task:', error); + } + }; + + if (loading) { + return ( + <div className="max-w-2xl mx-auto p-6"> + <h1 className="text-3xl font-bold mb-6">Task Manager</h1> + <div className="p-4 text-center">Loading tasks...</div> + </div> + ); + } + + if (error) { + return ( + <div className="max-w-2xl mx-auto p-6"> + <h1 className="text-3xl font-bold mb-6">Task Manager</h1> + <div className="p-4 bg-red-50 border border-red-200 rounded-lg"> + <p className="text-red-700">Error: {error}</p> + <button + onClick={fetchTasks} + className="mt-2 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" + > + Retry + </button> + </div> + </div> + ); + } + + return ( + <div className="max-w-2xl mx-auto p-6"> + <h1 className="text-3xl font-bold mb-6">Task Manager</h1> + + <form onSubmit={createTask} className="mb-8 space-y-4"> + <div> + <input + type="text" + placeholder="Task title" + value={newTask.title} + onChange={(e) => setNewTask({ ...newTask, title: e.target.value })} + className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + </div> + <div> + <textarea + placeholder="Task description (optional)" + value={newTask.description} + onChange={(e) => setNewTask({ ...newTask, description: e.target.value })} + className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + rows={3} + /> + </div> + <button + type="submit" + className="w-full bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors" + > + Add Task + </button> + </form> + + <div className="space-y-4"> + {tasks.length === 0 ? ( + <p className="text-gray-500 text-center">No tasks yet. Create your first task above!</p> + ) : ( + tasks.map((task) => ( + <div + key={task.id} + className={`p-4 border rounded-lg ${ + task.completed ? 'bg-green-50 border-green-200' : 'bg-white border-gray-200' + }`} + > + <div className="flex items-start justify-between"> + <div className="flex-1"> + <h3 + className={`font-semibold ${ + task.completed ? 'line-through text-gray-500' : 'text-gray-900' + }`} + > + {task.title} + </h3> + {task.description && ( + <p + className={`mt-1 ${ + task.completed ? 'line-through text-gray-400' : 'text-gray-600' + }`} + > + {task.description} + </p> + )} + {task.created_at && ( + <p className="text-xs text-gray-400 mt-2"> + Created: {new Date(task.created_at).toLocaleString()} + </p> + )} + </div> + <div className="flex space-x-2 ml-4"> + <button + onClick={() => toggleTask(task)} + className={`px-3 py-1 rounded text-sm ${ + task.completed + ? 'bg-yellow-500 text-white hover:bg-yellow-600' + : 'bg-green-500 text-white hover:bg-green-600' + }`} + > + {task.completed ? 'Undo' : 'Complete'} + </button> + <button + onClick={() => deleteTask(task.id)} + className="px-3 py-1 bg-red-500 text-white rounded text-sm hover:bg-red-600" + > + Delete + </button> + </div> + </div> + </div> + )) + )} + </div> + </div> + ); +}
\ No newline at end of file |
