The Projects Page

Project List

Let's create a component ProjectList in app/project-list.tsx that will show a nicely styled list of projects:

export function ProjectList({ projects }: { projects: { id: number, name: string }[] }) {
  return (
    <div className="my-8 mx-auto w-full max-w-2xl">
      {projects.map((project) => (
        <div className="flex items-center justify-between bg-white p-4 rounded-lg shadow-md mb-4 hover:shadow-lg transition-shadow duration-200 ease-in-out">
          <span className="text-lg font-semibold text-gray-800 hover:text-blue-500 transition-colors duration-150 ease-in-out">
            {project.name}
          </span>
        </div>
      ))}
    </div>
  );
}

Update the app/page.tsx file to retrieve the projects show the project list:

import { db } from '@/db';
import { projectTable } from '@/db/schema';
import { auth } from '@clerk/nextjs/server';
import { SignIn } from '@clerk/nextjs';
import { ProjectList } from './project-list';
import { eq } from 'drizzle-orm';

export default async function Home() {
  const { userId } = auth();

  if (userId === null) {
    // ...
  }

  const projects = await db.select().from(projectTable).where(eq(projectTable.userId, userId));

  return <ProjectList projects={projects} />;
}

Add a few projects with the correct user ID to the database and go to localhost:3000. You should see a project list containing the added projects.

Fixing a Lint

While has no syntax errors, no type errors and generally works correctly, there is one issue.

If you read this book carefully so far, you should theoretically be able to figure it out, but it might take a while. Let's use our awesome ESLint tool instead:

pnpm lint

You should see:

./src/app/project-list.tsx
9:9  Error: Missing "key" prop for element in iterator  react/jsx-key

Remember that if you want to render a list in React, you should give the individual elements a key prop. In this case, a good key prop would be the primary key from the database, so let's use that.

Add the key property to the project div in app/project-list.tsx like this:

// ...
<div
  key={project.id}
  className="flex items-center justify-between bg-white p-4 rounded-lg shadow-md mb-4 hover:shadow-lg transition-shadow duration-200 ease-in-out"
>
  <span className="text-lg font-semibold text-gray-800 hover:text-blue-500 transition-colors duration-150 ease-in-out">
    {project.name}
  </span>
</div>
// ...

If you rerun pnpm lint, you should see no more errors.

New Project Modal

Let's create a modal that will allow us to add new projects at app/new-project-modal.tsx:

"use client";

import * as React from "react";

interface FormElements extends HTMLFormControlsCollection {
  name: HTMLInputElement;
}

interface ProjectFormElement extends HTMLFormElement {
  readonly elements: FormElements;
}

export function NewProjectModal({
  onClose,
  onSubmit,
}: {
  onClose: () => void;
  onSubmit: (name: string) => Promise<void>;
}) {
  async function handleSubmit(event: React.FormEvent<ProjectFormElement>) {
    event.preventDefault();
    const name = event.currentTarget.elements.name.value.trim();
    await onSubmit(name);
  }

  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"
        >
          &times;
        </button>
        <form onSubmit={handleSubmit} className="space-y-6">
          <h2 className="text-xl font-semibold text-gray-800">Add Project</h2>
          <div>
            <label htmlFor="name" className="text-sm font-medium text-gray-600">
              Project Name
            </label>
            <input
              type="text"
              id="name"
              name="name"
              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 Project
          </button>
        </form>
      </div>
    </div>
  );
}

Let's create a file db/actions.ts containing the insertProject function:

'use server';

import { db } from '.';
import { projectTable } from './schema';

export async function insertProject(userId: string, name: string) {
  await db.insert(projectTable).values({ userId, name });
}

We need to show the modal in the project list by modifying the app/project-list.tsx file:

'use client';

import { insertProject } from '@/db/actions';
import { NewProjectModal } from './new-project-modal';

import * as React from 'react';
import { useRouter } from 'next/navigation';

export function ProjectList({
  userId,
  projects,
}: {
  userId: string,
  projects: { id: number, name: string }[],
}) {
  const router = useRouter();

  const [showModal, setShowModal] = React.useState(false);

  async function handleNewProject(name: string) {
    await insertProject(userId, name);
    setShowModal(false);
    router.refresh();
  }

  return (
    <div className="my-8 mx-auto w-full max-w-2xl">
      <button
        onClick={() => setShowModal(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 Project
      </button>

      {/* Project list here */}

      {showModal && (
        <NewProjectModal onSubmit={handleNewProject} onClose={() => setShowModal(true)} />
      )}
    </div>
  );
}

Finally, we need to update the app/page.tsx file since the ProjectList component now takes a user ID:

export default async function Home() {
  // ...

  return <ProjectList userId={userId} projects={projects} />;
}

Go to localhost:3000 and try adding a few projects using the "Add new project" button and the project modal.