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