diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..135c752 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,30 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + email String @unique + passwordHash String + verifiedAt DateTime? + emailVerificationToken String? + emailVerificationExpires DateTime? + passwordResetToken String? + passwordResetExpires DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Task { + id String @id @default(cuid()) + title String + description String? + completed Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} \ No newline at end of file diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts new file mode 100644 index 0000000..0370350 --- /dev/null +++ b/src/app/api/tasks/route.ts @@ -0,0 +1,141 @@ +import { NextResponse } from 'next/server'; +import { PrismaClient } from '@prisma/client'; +import { Task } from '@/types/task'; + +const prisma = new PrismaClient(); + +// GET /api/tasks - Get all tasks +export async function GET() { + try { + const tasks = await prisma.task.findMany({ + orderBy: { + createdAt: 'desc', + }, + }); + + return NextResponse.json(tasks); + } catch (error) { + console.error('Error fetching tasks:', error); + return NextResponse.json( + { error: 'Failed to fetch tasks' }, + { status: 500 } + ); + } +} + +// POST /api/tasks - Create a new task +export async function POST(request: Request) { + try { + const { title, description } = await request.json(); + + // Validate required fields + if (!title || typeof title !== 'string') { + return NextResponse.json( + { error: 'Title is required and must be a string' }, + { status: 400 } + ); + } + + const task = await prisma.task.create({ + data: { + title, + description: description || null, + }, + }); + + return NextResponse.json(task, { status: 201 }); + } catch (error) { + console.error('Error creating task:', error); + return NextResponse.json( + { error: 'Failed to create task' }, + { status: 500 } + ); + } +} + +// PUT /api/tasks/:id - Update a task +export async function PUT(request: Request, { params }: { params: { id: string } }) { + try { + const { id } = params; + const { title, description, completed } = await request.json(); + + // Validate required fields + if (!title || typeof title !== 'string') { + return NextResponse.json( + { error: 'Title is required and must be a string' }, + { status: 400 } + ); + } + + const task = await prisma.task.update({ + where: { id }, + data: { + title, + description: description || null, + completed, + }, + }); + + return NextResponse.json(task); + } catch (error) { + console.error('Error updating task:', error); + return NextResponse.json( + { error: 'Failed to update task' }, + { status: 500 } + ); + } +} + +// DELETE /api/tasks/:id - Delete a task +export async function DELETE(request: Request, { params }: { params: { id: string } }) { + try { + const { id } = params; + + await prisma.task.delete({ + where: { id }, + }); + + return NextResponse.json({ message: 'Task deleted successfully' }); + } catch (error) { + console.error('Error deleting task:', error); + return NextResponse.json( + { error: 'Failed to delete task' }, + { status: 500 } + ); + } +} + +// PATCH /api/tasks/:id/complete - Toggle task completion +export async function PATCH(request: Request, { params }: { params: { id: string } }) { + try { + const { id } = params; + + // First get the current task to determine its current completion status + const currentTask = await prisma.task.findUnique({ + where: { id }, + }); + + if (!currentTask) { + return NextResponse.json( + { error: 'Task not found' }, + { status: 404 } + ); + } + + // Toggle the completion status + const updatedTask = await prisma.task.update({ + where: { id }, + data: { + completed: !currentTask.completed, + }, + }); + + return NextResponse.json(updatedTask); + } catch (error) { + console.error('Error toggling task completion:', error); + return NextResponse.json( + { error: 'Failed to toggle task completion' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx new file mode 100644 index 0000000..baa04a0 --- /dev/null +++ b/src/components/TaskItem.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react'; + +interface Task { + id: string; + title: string; + description?: string; + completed: boolean; + createdAt: Date; + updatedAt: Date; +} + +interface TaskItemProps { + task: Task; + onToggleComplete: (id: string) => void; + onDelete: (id: string) => void; + onEdit: (task: Task) => void; +} + +export default function TaskItem({ task, onToggleComplete, onDelete, onEdit }: TaskItemProps) { + const [isEditing, setIsEditing] = useState(false); + const [editedTitle, setEditedTitle] = useState(task.title); + const [editedDescription, setEditedDescription] = useState(task.description || ''); + + const handleSave = () => { + onEdit({ ...task, title: editedTitle, description: editedDescription }); + setIsEditing(false); + }; + + const handleCancel = () => { + setEditedTitle(task.title); + setEditedDescription(task.description || ''); + setIsEditing(false); + }; + + if (isEditing) { + return ( +
+ setEditedTitle(e.target.value)} + className="w-full p-2 border rounded mb-2 text-lg font-semibold" + /> +