お問い合わせ
お問い合わせフォームです。入力をチェックしてから送信し、送信中・送信完了を画面で伝えます。
お問い合わせ
ここがポイント
- 1
送信中は、もう一度押せなくする
送っているあいだはボタンを押せなくして「送信中…」と出す。押せたままだと、同じお問い合わせが2回届いてしまう
- 2
送れたら、画面ごと切り替える
送信が終わったら、フォームを消して「送信しました」の画面を出す。いつ返事が来るかも書いておくと、相手が安心する
- 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> ) }