🐥

【Next.js】AppRouterでお問い合わせフォームを作ってみた!!

2023/12/09に公開

はじめに

Next.js 13でリリースされたAppRouterを活用してお問い合わせフォオームをインプット・アウトプットとして作ってみます。

使用技術

以下の物を使用

  • Next.js(AppRouter)
  • TypeScript
  • Chakra UI
  • MySQL
  • Docker

環境構築

空のフォルダを作成し以下のようなディレクトリ構成にする

.
├── .env
├── .gitignore
├── Dockerfile
├── app
├── docker-compose.yml
├── initdb.d
│   └── create-table.sql
└── mysql
    └── my.cnf

Dockerfileには以下を記述

FROM node:lts-alpine  
WORKDIR /app

docker-compose.ymlには以下を記述

version: '3'
services:
  app:
    container_name: app
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./app:/app
    command: sh -c "npm run dev"
    tty: true
    ports:
      - 3000:3000
    environment:
      - DB_HOST=${MYSQL_HOST}
      - DB_PORT=${MYSQL_PORT}
      - DB_USER=${MYSQL_USER}
      - DB_PASSWORD=${MYSQL_PASSWORD}
      - DB_DATABASE=${MYSQL_DATABASE}
    depends_on:
      - mysql
  mysql:
    container_name: db
    image: mysql:latest
    restart: always
    ports:
      - ${MYSQL_PORT}:${MYSQL_PORT}
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - TZ=${TZ} # タイムゾーン
    volumes:
      - ./initdb.d:/docker-entrypoint-initdb.d # DBの初期データ
      - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf # bashで日本語文字化けする問題の解決

/initdb.d/create-table.sqlは以下のように記述する

CREATE TABLE contact_table (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL,
    content_question TEXT NOT NULL,
    postdate DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)
);

/mysql/my.cnfは以下

[mysqld]
character_set_server = utf8mb4
collation-server=utf8mb4_unicode_ci

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4

.gitignore

.DS_Store

.env.local
.env

.env

MYSQL_HOST=mysql
MYSQL_USER=root
MYSQL_PORT=3306
MYSQL_PASSWORD=password
MYSQL_DATABASE=contactform
TZ=Asia/Tokyo

必要なファイルの作成ができたのでこれからNext.jsの環境を作成します。
以下を実行

docker compose run --rm app npx create-next-app
  • プロジェクト名は「.」を入力
  • TypeScriptを選択
  • ESLintを選択
  • Tailwind CSSを選択
  • src/ディレクトリを使うを選択
  • App Routerを選択

これで/appフォルダ内にNext.jsの環境ができました。

試しに起動するか確認しましょう

docker compose down # 念の為、コンテナを修了
docker compose up -d

localhost:3000にアクセスするとNext.jsのページが表示されると思います。

データベースの確認は以下のコマンドでできます。

docker exec -it db sh # コンテナの中に入る
mysql -u root -p # DBにログイン
use contactform; # データベースを選択
show tables; # テーブル一覧を表示

exit # 表示できたらSQLを終了
exit # コンテナからも出る

これでcontact_tableのテーブルが表示されればうまくデータベースは動いています。

次に使用するライブラリのインストールをします。

docker exec -it ap sh # コンテナの中に入る
npm i framer-motion mysql2
npm i -D @chakra-ui/react @emotion/react @emotion/styled @types/mysql
exit # インストールできたらコンテナから出る

初期設定

/app/globals.cssを削除します。

/app/layout.tsxを以下のように編集します。

- import './globals.css'
import { Inter } from 'next/font/google'
+ import { ChakraProvider } from "@chakra-ui/react"

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
-       <body className={inter.className}>{children}</body>
+       <body className={inter.className}>
+           <ChakraProvider>{children}</ChakraProvider>
+       </body>
    </html>
  )
}

フォーム画面の実装

/app/page.tsxを以下のようにします。

"use client"
import { Box, Button, Center, FormLabel, Heading, Input, Textarea } from "@chakra-ui/react"
import { ChangeEvent, FormEvent, useState } from "react"

export default function Home() {
  const [userName, setUserName] = useState<string>("")
  const userNameChange = (e: ChangeEvent<HTMLInputElement>) => {
    setUserName(e.currentTarget.value)
  }
  const [userEmail, setUserEmail] = useState<string>("")
  const userEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
    setUserEmail(e.currentTarget.value)
  }
  const [userContent, setUserContent] = useState<string>("")
  const userContentChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
    setUserContent(e.currentTarget.value)
  }

  const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()

    try {
      // 送信処理
      const response = await fetch("/api/send", {
        method: "POST",
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          name: userName,
          email: userEmail,
          content: userContent
        })
      })
  
      const json = await response.json()
      console.log(json.message);
      
      if (response.status === 200) {
        setUserName("")
        setUserEmail("")
        setUserContent("")
      }
    } catch (error) {
      console.log("送信失敗", error);
    }

    
  }
  return (
    <Center w="full" flexDir="column">
      <Heading py={5}>お問合せフォーム</Heading>
      <form onSubmit={onSubmit} style={{width: "100%"}}>
        <Center gap={3} flexDir="column">
          <Box w="40%" minW="250px">
            <FormLabel htmlFor='name'>名前</FormLabel>
            <Input id='name' placeholder='名前' value={userName} onChange={userNameChange}/>
          </Box>
          <Box w="40%" minW="250px">
            <FormLabel htmlFor='email'>メールアドレス</FormLabel>
            <Input id='email' placeholder='メールアドレス' value={userEmail} onChange={userEmailChange}/>
          </Box>
          <Box w="40%" minW="250px">
            <FormLabel htmlFor='content'>内容</FormLabel>
            <Textarea id='content' placeholder='内容' value={userContent} onChange={userContentChange}/>
          </Box>
          <Box w="40%" minW="250px" textAlign="center">
            <Button type="submit">送信</Button>
          </Box>
        </Center>
      </form>
    </Center>
  )
}

データベースとの接続

/app/lib/dbフォルダを作成し、/db内にconnection.tsを作成します。

// src/lib/connection.ts

import mysql from 'mysql2/promise';

const mysql_connection = async () =>
  await mysql.createConnection({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_DATABASE,
  });

export default mysql_connection;

送信のAPIの実装

/app/api/sendのパスでフォルダを作り、/send内にroute.tsを作成します。

import mysql_connection from "@/lib/db/connection";
import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  const body = await request.json();

  if (!body.name || !body.email || !body.content) {
    return new Response(JSON.stringify({ message: "入力値が不正です。" }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }

  try {
    const connection = await mysql_connection();

    const query =
      "INSERT INTO contact_table (name, email, content_question) VALUES (?, ?, ?)";
    await connection.execute(query, [body.name, body.email, body.content]);

    return new Response(JSON.stringify({ message: "送信に成功しました。" }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  } catch (error) {
    return new Response(JSON.stringify({ message: "送信に失敗しました。" }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
}

これで送信機能の実装ができました。
試しにlocalhost:3000にアクセスしてフォームを送信してみましょう。

送信できたら以下のコマンドでDBのコンテナに入ってテーブルにデータが追加されているか確認しましょう。

docker exec -it db sh # コンテナの中に入る
mysql -u root -p # DBにログイン
use contactform; # データベースを選択
select * from contact_table;

送信したデータが存在すればこれで完成です。

一覧表示画面

/app/contents/page.tsxを以下のように作成

"use client"
import {useState, useEffect} from "react"
import {Center, Heading, Table, Thead, Tbody, Tr, Th ,Td} from "@chakra-ui/react"

export interface responceData {
    id: number
    name: string
    email: string
    content_question: string
    postdate: string
}
const Page = () => {

    const [contents, setContents] = useState<responceData[]>([])
    const initFetch = async () => {
        const response = await fetch("/api/contents")
        const data = await response.json()
        setContents(data.contents)
    }

    useEffect(() => {
        initFetch()
    },[])
    
    return <Center w="full" flexDirection="column">
        <Heading py={5}>お問合せ一覧</Heading>
        <Table>
            <Thead>
                <Tr>
                    <Th>No.</Th>
                    <Th>名前</Th>
                    <Th>メール</Th>
                    <Th>内容</Th>
                    <Th>時間</Th>
                </Tr>
            </Thead>
            <Tbody>
                {contents.map((item, index) => (
                    <Tr key={index}>
                        <Td>{item.id}</Td>
                        <Td>{item.name}</Td>
                        <Td>{item.email}</Td>
                        <Td>{item.content_question}</Td>
                        <Td>{item.postdate}</Td>
                    </Tr>
                ))}
            </Tbody>
        </Table>
    </Center>
}

export default Page

一覧取得API

/app/api/contents/route.tsを以下のように作成

import mysql_connection from "@/lib/db/connection";

export async function GET() {
  try {
    const connection = await mysql_connection();
    const result = await connection.query("SELECT * from contact_table");
    connection.end();
    return new Response(
      JSON.stringify({ message: "取得に成功しました。", contents: result[0] }),
      {
        status: 200,
        headers: { "Content-Type": "application/json" },
      }
    );
  } catch (error) {
    return new Response(JSON.stringify({ message: "取得に失敗しました。" }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
}

これでlocalhost:3000/contentに行けば一覧表示画面、localhost:3000/api/contentsに行けば一覧結果が表示されます。

リポジトリのURL
cloneしてお試しください。

GitHubで編集を提案

Discussion