133 lines
6 KiB
TypeScript
133 lines
6 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
// ฟอร์ม login (mock e-Court) — เรียก server action login แล้ว router.replace("/")
|
||
|
|
// (pattern ตาม UploadFileForm: useTransition + error state + ข้อความ fallback ตอน transport fail)
|
||
|
|
import { useRef, useState, useTransition } from "react";
|
||
|
|
import { useRouter } from "next/navigation";
|
||
|
|
|
||
|
|
import { login } from "@/app/actions/auth";
|
||
|
|
import Select from "@/components/Select";
|
||
|
|
import { MOCK_USERS } from "@/utils/user";
|
||
|
|
|
||
|
|
// text input ของแอป — text-base (16px) เสมอ: กัน iOS auto-zoom ตอน focus บน iPad
|
||
|
|
const INPUT_CLASS =
|
||
|
|
"mt-1 block w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-base text-slate-900 placeholder:text-slate-400 transition focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:cursor-not-allowed disabled:bg-slate-50 aria-invalid:border-red-600 aria-invalid:focus:ring-red-600/30";
|
||
|
|
// dropdown ผู้ใช้ — เหมือน INPUT_CLASS แต่ไม่มี mt-1 (Select ครอบ div.relative: มาร์จินที่ wrapper ข้างนอกแทน)
|
||
|
|
const SELECT_CLASS =
|
||
|
|
"block w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-base text-slate-900 transition focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:cursor-not-allowed disabled:bg-slate-50";
|
||
|
|
|
||
|
|
export default function LoginForm() {
|
||
|
|
const router = useRouter();
|
||
|
|
const passwordRef = useRef<HTMLInputElement | null>(null);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
const [pending, startTransition] = useTransition();
|
||
|
|
|
||
|
|
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const form = e.currentTarget;
|
||
|
|
setError(null);
|
||
|
|
const usernameInput = form.elements.namedItem("username") as HTMLSelectElement;
|
||
|
|
const passwordInput = form.elements.namedItem("password") as HTMLInputElement;
|
||
|
|
startTransition(async () => {
|
||
|
|
try {
|
||
|
|
const result = await login(usernameInput.value, passwordInput.value);
|
||
|
|
if (result.ok) {
|
||
|
|
router.replace("/");
|
||
|
|
} else {
|
||
|
|
setError(result.error);
|
||
|
|
// คง username ไว้ (โอกาสพลาดอยู่ที่ password) — ล้างแล้วโฟกัสให้พิมพ์ทับได้ทันที
|
||
|
|
passwordInput.value = "";
|
||
|
|
passwordRef.current?.focus();
|
||
|
|
passwordRef.current?.select();
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
setError("เข้าสู่ระบบไม่สำเร็จ — ลองใหม่อีกครั้ง");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="w-full max-w-sm rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||
|
|
{/* brand lockup เดียวกับ header หน้ารายการ (ลดขนาดตัวอักษรให้พอการ์ดแคบ) */}
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-primary text-base font-bold text-white shadow-sm">
|
||
|
|
JN
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p className="text-lg font-bold leading-tight text-primary">Judge's Note</p>
|
||
|
|
<p className="text-xs text-slate-500">ระบบ PDF Annotation สำหรับ e-Court</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h1 className="mt-6 text-lg font-semibold text-slate-900">เข้าสู่ระบบ</h1>
|
||
|
|
|
||
|
|
<form onSubmit={onSubmit} className="mt-4">
|
||
|
|
<div>
|
||
|
|
<label htmlFor="username" className="text-sm font-semibold text-slate-700">
|
||
|
|
ชื่อผู้ใช้
|
||
|
|
</label>
|
||
|
|
<div className="mt-1">
|
||
|
|
<Select
|
||
|
|
id="username"
|
||
|
|
name="username"
|
||
|
|
autoComplete="username"
|
||
|
|
required
|
||
|
|
autoFocus
|
||
|
|
disabled={pending}
|
||
|
|
defaultValue=""
|
||
|
|
className={SELECT_CLASS}
|
||
|
|
>
|
||
|
|
{/* placeholder บังคับเลือก — ไม่ preselect คนแรก กันผู้ทดสอบ login ผิดคนโดยไม่ตั้งใจ */}
|
||
|
|
<option value="" disabled>
|
||
|
|
เลือกผู้ใช้
|
||
|
|
</option>
|
||
|
|
{MOCK_USERS.map((u) => (
|
||
|
|
<option key={u} value={u}>
|
||
|
|
{u}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="mt-4">
|
||
|
|
<label htmlFor="password" className="text-sm font-semibold text-slate-700">
|
||
|
|
รหัสผ่าน
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
id="password"
|
||
|
|
name="password"
|
||
|
|
type="password"
|
||
|
|
autoComplete="current-password"
|
||
|
|
required
|
||
|
|
disabled={pending}
|
||
|
|
aria-invalid={!!error || undefined}
|
||
|
|
ref={passwordRef}
|
||
|
|
className={INPUT_CLASS}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
disabled={pending}
|
||
|
|
className="mt-6 w-full rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-white transition hover:opacity-90 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
|
||
|
|
>
|
||
|
|
{pending ? "กำลังตรวจสอบ…" : "เข้าสู่ระบบ"}
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{/* รหัสผ่านทดสอบ — รายชื่อเห็นใน dropdown แล้ว เหลือบอกแค่รหัสผ่าน (mock ไม่มีระบบสมัคร) */}
|
||
|
|
<p className="mt-5 rounded-md bg-slate-50 px-3 py-2 text-xs text-slate-500">
|
||
|
|
รหัสผ่านทดสอบ:{" "}
|
||
|
|
<span className="font-semibold text-slate-700">Demo1234</span>
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{error && (
|
||
|
|
<p role="alert" className="mt-3 text-xs text-red-600">
|
||
|
|
{error}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|