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:
parent
b71d82d32e
commit
84745822f5
30
prisma/schema.prisma
Normal file
30
prisma/schema.prisma
Normal 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
141
src/app/api/tasks/route.ts
Normal 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
109
src/components/TaskItem.tsx
Normal 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
8
src/types/task.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface Task {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
completed: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user