Comment form with database

December 14, 2023 (10mo ago)

We will store the comments data in the planetscale database table. So create a new database and generate the credentials of database url to connect to it. If you have existing database, you will require a new table to the database.

Install dependencies

pnpm add prisma prisma-kysely kysely kysely-planetscale use-server

Connection to database

We will connect to the database to create a new table for storing the data of the comments.

Go to your planetscale dashboard and copy the generated database url and paste in the .env file variable, it should look like DATABASE_URL: "mysql://....".

\\ prisma/scheme.prisma

datasource db {
    provider     = "mysql"
    url          = env("DATABASE_URL")
    relationMode = "prisma"
}

generator kysely {
    provider = "prisma-kysely"
    output   = "../types"
    fileName = "db-types.ts"
}

model comment {
    id    BigInt @id @default(autoincrement())
    slug  String @db.VarChar(128)
    name String @db.VarChar(50)
    body String @db.VarChar(700)
    email String
    image String?
    created_at DateTime @default(now()) @db.DateTime(6)
}
  • Run npx prisma db push
  • This will generate a file named types/db-types.ts

Query to database

  • Create a new file which exports queryBuilder to query the database in the app.
import { Kysely } from "kysely";
import { PlanetScaleDialect } from "kysely-planetscale";
import { DB } from "types/db-types";

export const queryBuilder = new Kysely<DB>({
  dialect: new PlanetScaleDialect({
    url: process.env.DATABASE_URL,
  }),
});

Comment form

"use client";

import { useRef } from "react";
import { saveCommentEntry } from "lib/actions";
import { experimental_useFormStatus as useFormStatus } from "react-dom";
export default function CommentForm({ slug }: { slug: string }) {
  const formRef = useRef<HTMLFormElement>(null);
  const { pending } = useFormStatus();
  return (
    <>
      <form
        style={{ opacity: !pending ? 1 : 0.5 }}
        className='w-full max-w-full mb-8 space-y-1'
        ref={formRef}
        action={async (formData) => {
          await saveCommentEntry(formData, slug);
          formRef.current?.reset();
        }}
      >
        <div>
          <div className='relative'>
            <input
              type='text'
              placeholder='Enter your name'
              aria-label='Enter your name'
              disabled={pending}
              name='name'
              required
              className='p-2 w-full border border-[var(--border)] outline-[var(--border)] rounded-md bg-[var(--primaryforeground)] text-[var(--primary)]'
            />
            <input
              type='email'
              placeholder='Enter your email'
              aria-label='Enter your email'
              disabled={pending}
              name='email'
              required
              className='p-2 w-full border border-[var(--border)] outline-[var(--border)] rounded-md bg-[var(--primaryforeground)] text-[var(--primary)]'
            />
            <input
              type='text'
              placeholder='Enter your image url'
              aria-label='Image'
              disabled={pending}
              name='image'
              // required
              className='p-2 w-full border border-[var(--border)] outline-[var(--border)] rounded-md bg-[var(--primaryforeground)] text-[var(--primary)]'
            />
            <textarea
              rows={3}
              placeholder='Enter your comment'
              aria-label='Enter your comment...'
              disabled={pending}
              name='entry'
              required
              className='p-2 w-full border border-[var(--border)] outline-[var(--border)] rounded-md bg-[var(--primaryforeground)] text-[var(--primary)]'
            />
          </div>
        </div>
        <div className='flex items-center mt-2 justify-between'>
          <button
            className='inline-block px-2 py-1 bg-[var(--offset2)] bg-opacity-60	 rounded-lg text-sm cursor-pointer'
            disabled={pending}
            type='submit'
          >
            Submit{" "}
          </button>
        </div>
      </form>
    </>
  );
}

Now create a function which pushes the comment form entries to the database.

"use server";

import { queryBuilder } from "lib/db";
import { revalidatePath } from "next/cache";
export async function saveCommentEntry(formData: FormData, slug: string) {
  const nameentry = formData.get("name")?.toString() || "";
  const emailentry = formData.get("email")?.toString() || "";
  const imageentry = formData.get("image")?.toString() || "";
  const entry = formData.get("entry")?.toString() || "";

  const name = nameentry.slice(0, 100);
  const email = emailentry.slice(0, 100);
  const image = emailentry.slice(0, 500);
  const body = entry.slice(0, 500);

  await queryBuilder
    .insertInto("comment")
    .values({ email, body, name, image, slug })
    .execute();

  revalidatePath(`/${slug}`);
}

Final componet which displays comment form and comment entries.

import { queryBuilder } from "lib/db";
import { Suspense } from "react";
import Image from "next/image";
import CommentForm from "./Form";
async function getComment(slug: string) {
  const data = await queryBuilder
    .selectFrom("comment")
    .selectAll()
    .where("slug", "=", `${slug}`)
    .select(["body", "name", "image", "email", "id", "created_at"])
    .orderBy("created_at", "desc")
    .limit(100)
    .execute();
  return data;
}

export const dynamic = "force-dynamic";
export const runtime = "edge";

export default async function CommentBox({ slug }: { slug: string }) {
  let entries;
  try {
    const [commentRes] = await Promise.allSettled([getComment(slug)]);

    if (commentRes.status === "fulfilled" && commentRes.value[0]) {
      entries = commentRes.value;
    } else {
      console.error(commentRes);
    }
  } catch (error) {
    console.error(error);
  }
  return (
    <>
      <CommentForm slug={slug} />

      <Suspense fallback={<p>Loading comments...</p>}>
        {entries === undefined ? (
          <p className='my-2'>No comments. Be the first one to comment.</p>
        ) : (
          entries.map((entry) => (
            <div
              key={entry.id}
              className='border-b border-[var(--border)] my-4 prose dark:prose-invert'
            >
              <div className='grid grid-cols-12 w-full'>
                <div className='flex rounded-xl col-span-12'>
                  <div className='flex h-8 w-8 bg-[var(--offset)] items-center justify-center overflow-hidden rounded-full flex-shrink-0'>
                    {entry.image ? (
                      <Image
                        src={entry.image}
                        alt={entry.name}
                        width={28}
                        height={28}
                        className='rounded-full self-centered'
                      />
                    ) : (
                      <svg
                        fill='none'
                        viewBox='0 0 24 24'
                        strokeWidth={1.5}
                        stroke='currentColor'
                        className='w-[24px] h-[24px] self-centered'
                      >
                        <path
                          strokeLinecap='round'
                          strokeLinejoin='round'
                          d='M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z'
                        />
                      </svg>
                    )}
                  </div>
                  <div className='ml-2 w-full'>
                    <div className='flex w-full items-start justify-between'>
                      <span className='font-semibold'>{entry.name}</span>
                      <Date> {entry.created_at} </Date>
                    </div>
                    <div className='mt-1'>{entry.body}</div>
                  </div>
                </div>
              </div>
            </div>
          ))
        )}
      </Suspense>
    </>
  );
}

Include the component as <CommentBox slug={slug} /> in your blog page.