Task Page
Schema Update
We now have a page where we can show the created projects. However, this is not terribly useful as long as we can't add tasks to the projects.
First, we need a place to store the tasks in our database.
Create a new task table in db/schema.ts
:
import { integer /*...*/ } from 'drizzle-orm/pg-core';
// ...
export const taskTable = pgTable('task', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
description: text('description').notNull(),
status: text('status').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
projectId: integer('project_id')
.notNull()
.references(() => projectTable.id),
});
Generate the migration:
pnpm db:generate
Review the migration (which might be something like db/migrations/0001_loose_wonder_man.sql
):
CREATE TABLE IF NOT EXISTS "task" (
"id" serial PRIMARY KEY NOT NULL,
"title" text NOT NULL,
"description" text NOT NULL,
"status" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"project_id" integer NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "task" ADD CONSTRAINT "task_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "project"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
Execute the migration:
pnpm db:migrate
Check that the task
table is present in the database together with the right columns.
Task Page
Now let's create a page containing the tasks of a given project at app/project/[id]/page.tsx
:
import { db } from '@/db';
import { projectTable, taskTable } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { auth } from '@clerk/nextjs/server';
export default async function Project({ params: { id } }: { params: { id: number } }) {
const { userId } = auth();
const projects = await db.select().from(projectTable).where(eq(projectTable.id, id));
const project = projects[0];
if (project.userId !== userId) {
return <h1>Not allowed to access project</h1>;
}
const tasks = await db.select().from(taskTable).where(eq(taskTable.projectId, id));
return (
<div>
{tasks.map((task) => (
<p key={task.id}>{task.title}</p>
))}
</div>
);
}
Add a few tasks to the project with the ID 1
and go to localhost:3000/project/1
- you should see these tasks.
However, the UX is currently quite ugly, so let's improve it.
Task List
Create the a TaskList
component at app/project/[id]/task-list.tsx
:
export function TaskList({
tasks,
}: {
tasks: { id: number, title: string, description: string, status: string }[],
}) {
return (
<div className="my-8 mx-auto w-full max-w-2xl">
{tasks.map((task) => (
<div
key={task.id}
className="flex flex-col bg-white p-4 rounded-lg shadow-md mb-4 hover:shadow-lg transition-shadow duration-200 ease-in-out"
>
<h3 className="text-lg font-semibold text-gray-800 mb-2">{task.title}</h3>
<p className="text-gray-600 mb-4">{task.description}</p>
<p className="text-sm text-blue-500">{task.status}</p>
</div>
))}
</div>
);
}
Use the TaskList
in app/project/[id]/page.tsx
:
// ...
import { TaskList } from './task-list';
export default async function Project({ params: { id } }: { params: { id: number } }) {
// ...
return <TaskList tasks={tasks} />;
}
New Task Modal
Finally, let's create a modal that will allow us to add new tasks.
Create a file app/project/[id]/new-task-modal.tsx
:
"use client";
interface FormElements extends HTMLFormControlsCollection {
title: HTMLInputElement;
description: HTMLInputElement;
}
interface TaskFormElement extends HTMLFormElement {
readonly elements: FormElements;
}
export function NewTaskModal({
onClose,
onSubmit,
}: {
onClose: () => void;
onSubmit: (title: string, description: string) => Promise<void>;
}) {
async function handleSubmit(event: React.FormEvent<TaskFormElement>) {
event.preventDefault();
const title = event.currentTarget.elements.title.value.trim();
const description = event.currentTarget.elements.description.value.trim();
await onSubmit(title, description);
}
return (
<div className="fixed inset-0 bg-gray-900 bg-opacity-50 flex justify-center items-center px-4">
<div className="relative w-full max-w-md bg-white p-6 rounded-lg shadow-lg">
<button
onClick={onClose}
className="absolute top-0 right-0 m-4 text-gray-400 hover:text-gray-600 transition duration-150 ease-in-out"
>
×
</button>
<form onSubmit={handleSubmit} className="space-y-6">
<h2 className="text-xl font-semibold text-gray-800">Add Task</h2>
<div>
<label
htmlFor="title"
className="text-sm font-medium text-gray-600"
>
Title
</label>
<input
type="text"
id="title"
name="title"
className="mt-2 block w-full px-4 py-3 bg-gray-50 rounded-md border-transparent focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50"
/>
</div>
<div>
<label
htmlFor="description"
className="text-sm font-medium text-gray-600"
>
Description
</label>
<input
type="text"
id="description"
name="description"
className="mt-2 block w-full px-4 py-3 bg-gray-50 rounded-md border-transparent focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50"
/>
</div>
<button
type="submit"
className="w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-gradient-to-r from-blue-500 to-teal-400 hover:from-blue-600 hover:to-teal-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-150 ease-in-out"
>
Add Task
</button>
</form>
</div>
</div>
);
}
Let's add a new database function insertTask
in db/actions.ts
:
// ...
import { taskTable /*...*/ } from './schema';
// ...
export async function insertTask(title: string, description: string, projectId: number) {
await db.insert(taskTable).values({ title, description, status: 'inprogress', projectId });
}
Use the new task modal in the app/project/[id]/task-list.tsx
file:
'use client';
import { insertTask } from '@/db/actions';
import { NewTaskModal } from './new-task-modal';
import * as React from 'react';
import { useRouter } from 'next/navigation';
export function TaskList({
projectId,
tasks,
}: {
projectId: number,
tasks: { id: number, title: string, description: string, status: string }[],
}) {
const [showNewTaskModal, setShowNewTaskModal] = React.useState(false);
const router = useRouter();
async function handleNewTask(title: string, description: string) {
await insertTask(title, description, projectId);
setShowNewTaskModal(false);
router.refresh();
}
return (
<div className="my-8 mx-auto w-full max-w-2xl">
<button
onClick={() => setShowNewTaskModal(true)}
className="mb-6 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded shadow hover:shadow-md transition duration-200 ease-in-out"
>
Add New Task
</button>
{/* Task list */}
{showNewTaskModal && (
<NewTaskModal onSubmit={handleNewTask} onClose={() => setShowNewTaskModal(false)} />
)}
</div>
);
}
Since the TaskList
component takes a projectId
prop, we need to update app/project/[id]/page.tsx
:
export default async function Project(/* ...*/) {
// ...
return <TaskList projectId={id} tasks={tasks} />;
}
You should now be able to use the "Add new task" button and the modal to add new tasks to the project.