お問い合わせ

お問い合わせフォームです。入力をチェックしてから送信し、送信中・送信完了を画面で伝えます。

お問い合わせ

ここがポイント

  1. 1

    送信中は、もう一度押せなくする

    送っているあいだはボタンを押せなくして「送信中…」と出す。押せたままだと、同じお問い合わせが2回届いてしまう

  2. 2

    送れたら、画面ごと切り替える

    送信が終わったら、フォームを消して「送信しました」の画面を出す。いつ返事が来るかも書いておくと、相手が安心する

  3. 3

    書ける量を、先に見せる

    本文の下に「今の文字数 / 上限」を出す。上限をこえたら赤にする。送ってから「長すぎます」と言われるより親切

使った道具

押すと、第2部のくらべるページへ

コードを書き換えてみる

コードを書き換えると、右の見た目が変わります

import { useState } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { CircleCheckIcon } from "lucide-react"
import { Button } from "./components/ui/button"
import { Input } from "./components/ui/input"
import { Label } from "./components/ui/label"

// お問い合わせの種類
const kinds = ["使い方について", "不具合の報告", "その他"]

// 入力チェックの決まり(Zod)
const schema = z.object({
  name: z.string().min(1, "お名前を入力してください"),
  email: z.string().email("メールアドレスの形で入力してください"),
  kind: z.enum(kinds, { error: "種類を選んでください" }),
  message: z.string().min(10, "10文字以上で書いてください").max(500, "500文字以内で書いてください"),
})

// 送信する…ふりをする関数(1.5秒かかる)
function send(values) {
  return new Promise((resolve) => setTimeout(resolve, 1500))
}

// ラベル・入力欄・エラーの文を、1つのまとまりにする
function Field({ id, label, error, children }) {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor={id}>{label}</Label>
      {children}
      {error && <p className="text-sm text-destructive">{error}</p>}
    </div>
  )
}

export default function App() {
  // ① 送信が終わったか(最初は false)。今は決まった値。ここを state にする
  const [sent, setSent] = useState(false)

  const { register, handleSubmit, watch, reset, formState: { errors, isSubmitting } } = useForm({
    resolver: zodResolver(schema),
    defaultValues: { name: "", email: "", kind: undefined, message: "" },
  })

  // 本文の今の文字数を数えるために、本文を見張る
  const message = watch("message")

  // 「送信する」を押して、入力チェックが通ったら動く
  async function onSubmit(values) {
    await send(values) // 送信が終わるまで待つ(1.5秒)
    // ② ここに:送信が終わったので、sent を true にする
    setSent(true)
  }

  // ③ 送信が終わったら、完了の画面を先に返す。今は決まった値(false)なので、いつもフォームになる
  //    ここに:false を、「送信が終わったか」に変える
  if (sent) {
    return (
      <div className="mx-auto my-6 w-full max-w-lg rounded-xl border bg-card p-10 text-center text-card-foreground">
        <CircleCheckIcon className="mx-auto size-12 text-emerald-500" />
        <p className="mt-4 text-lg font-semibold">送信しました</p>
        <p className="mt-2 text-sm text-muted-foreground">
          お問い合わせありがとうございます。2営業日以内に、メールでお返事します。
        </p>
        {/* ④ ここに:押したら、入力を空に戻して(reset)、フォームの画面に戻す(sent を false に) */}
        <Button variant="outline" className="mt-6" onClick={() => {
          reset()
          setSent(false)
        }}>
          もう一度送る(デモ用)
        </Button>
      </div >
    )
  }
  return (
    <form
      onSubmit={handleSubmit(onSubmit)}
      className="mx-auto my-6 flex w-full max-w-lg flex-col gap-5 rounded-xl border bg-card p-6 text-card-foreground"
    >
      <div>
        <h1 className="text-lg font-semibold">お問い合わせ</h1>
        <p className="mt-1 text-sm text-muted-foreground">ご質問やご意見を、お気軽にお送りください。</p>
      </div>

      <Field id="name" label="お名前" error={errors.name?.message}>
        <Input id="name" placeholder="山田 太郎" {...register("name")} />
      </Field>
      <Field id="email" label="メールアドレス" error={errors.email?.message}>
        <Input id="email" type="email" placeholder="taro@example.com" {...register("email")} />
      </Field>

      <div className="flex flex-col gap-2">
        <Label>種類</Label>
        <div className="flex flex-wrap gap-2">
          {kinds.map((kind) => (
            <label
              key={kind}
              className="cursor-pointer rounded-full border px-3 py-1.5 text-sm has-checked:border-primary has-checked:bg-primary has-checked:text-primary-foreground"
            >
              <input type="radio" value={kind} className="sr-only" {...register("kind")} />
              {kind}
            </label>
          ))}
        </div>
        {errors.kind && <p className="text-sm text-destructive">{errors.kind.message}</p>}
      </div>

      <Field id="message" label="お問い合わせ内容" error={errors.message?.message}>
        <textarea
          id="message"
          rows={5}
          placeholder="10文字以上で書いてください"
          className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
          {...register("message")}
        />
        {/* ⑦ ここに:500文字をこえたら赤(text-destructive)、それ以外は灰色(text-muted-foreground) */}
        <p className={message.length > 500 ? "text-right text-xs text-destructive" : "text-right text-xs text-muted-foreground"}>
          {/* ⑥ ここに:0 を、今の本文の文字数にする */}
          {message.length}  / 500
        </p>
      </Field>

      {/* ⑤ ここに:送信中(isSubmitting が true)は押せない。文字は「送信中…」、それ以外は「送信する」 */}
      <Button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "送信中…" : "送信する"}
      </Button>
    </form>
  )
}