聯絡我們

聯絡我們的表單。送出前先驗證輸入,並在畫面上顯示「送出中」和「送出完成」。

聯絡我們

重點

  1. 1

    送出中讓按鈕無法再按

    送出期間讓按鈕無法點擊,並顯示「送出中…」。如果按鈕還能按,同一則詢問可能會收到兩次。

  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))
}

// 把標籤、輸入欄位和錯誤訊息組合在一起
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)
  }

  // ③ 傳送完成後,先回傳完成畫面
  //    條件:是否已傳送完成
  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="zhiming.lin@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"}>
          {/* ⑥ 內文目前的字數 */}
          {message.length}  / 500
        </p>
      </Field>

      {/* ⑤ 傳送中(isSubmitting 為 true)時不能點擊。傳送中顯示「傳送中…」,否則顯示「傳送」 */}
      <Button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "傳送中…" : "傳送"}
      </Button>
    </form>
  )
}