feat(02-01): implement database schema and API endpoints for task management

- Added Task model to Prisma schema with id, title, description, completed, createdAt, updatedAt fields
- Created POST /api/tasks endpoint for task creation
- Created GET /api/tasks endpoint for retrieving all tasks
- Created PUT /api/tasks/:id endpoint for updating tasks
- Created DELETE /api/tasks/:id endpoint for deleting tasks
- Created PATCH /api/tasks/:id/complete endpoint for toggling task completion
- Created TaskItem component for displaying and editing tasks
- Defined Task TypeScript interface
- All endpoints include proper validation and error handling
This commit is contained in:
mARTin 2026-01-25 11:17:18 +01:00
parent b71d82d32e
commit 84745822f5
4 changed files with 288 additions and 0 deletions

30
prisma/schema.prisma Normal file
View File

@ -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
}

141
src/app/api/tasks/route.ts Normal file
View File

@ -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 }
);
}
}

109
src/components/TaskItem.tsx Normal file
View File

@ -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 (
<div className="border rounded-lg p-4 mb-2 bg-white shadow-sm">
<input
type="text"
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
className="w-full p-2 border rounded mb-2 text-lg font-semibold"
/>
<textarea
value={editedDescription}
onChange={(e) => setEditedDescription(e.target.value)}
className="w-full p-2 border rounded mb-2"
rows={2}
/>
<div className="flex justify-end space-x-2">
<button
onClick={handleCancel}
className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Save
</button>
</div>
</div>
);
}
return (
<div className="border rounded-lg p-4 mb-2 bg-white shadow-sm">
<div className="flex items-start">
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggleComplete(task.id)}
className="mt-1 mr-3 h-5 w-5"
/>
<div className="flex-1">
<h3 className={`text-lg font-semibold ${task.completed ? 'line-through text-gray-500' : ''}`}>
{task.title}
</h3>
{task.description && (
<p className={`mt-1 ${task.completed ? 'line-through text-gray-500' : 'text-gray-700'}`}>
{task.description}
</p>
)}
<p className="text-xs text-gray-500 mt-2">
Created: {new Date(task.createdAt).toLocaleDateString()}
</p>
</div>
<div className="flex space-x-2 ml-2">
<button
onClick={() => {
setIsEditing(true);
}}
className="text-blue-500 hover:text-blue-700"
>
Edit
</button>
<button
onClick={() => onDelete(task.id)}
className="text-red-500 hover:text-red-700"
>
Delete
</button>
</div>
</div>
</div>
);
}

8
src/types/task.ts Normal file
View File

@ -0,0 +1,8 @@
export interface Task {
id: string;
title: string;
description?: string;
completed: boolean;
createdAt: Date;
updatedAt: Date;
}