143 lines
6.8 KiB
TypeScript
143 lines
6.8 KiB
TypeScript
"use client";
|
|
|
|
// ฟอร์ม login (mock e-Court) — เรียก server action login แล้ว router.replace(next ?? "/")
|
|
// (pattern ตาม UploadFileForm: useTransition + error state + ข้อความ fallback ตอน transport fail)
|
|
// next = path วนกลับหลัง login (N8 D5 — ผ่าน sanitizeNextPath จากหน้า login มาแล้ว)
|
|
// Genesis (docs/DESIGN.md): การ์ด surface 12px radius ขอบ line ไม่มีเงาระดับ static /
|
|
// input ผ่าน .genesis-input / ปุ่ม primary hover ขึ้น 1px + glow ฟ้า (shadow-btn-glow)
|
|
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
|
|
// (ทับ text-sm ของ .genesis-input — utility อยู่หลัง @layer components ชนะเสมอ)
|
|
// error ตาม spec Inputs: ขอบเปลี่ยนเป็นแดง (ring ยังเป็นฟ้าตาม focus ring หลัก)
|
|
const INPUT_CLASS =
|
|
"genesis-input mt-1 block w-full text-base transition aria-invalid:border-error";
|
|
// dropdown ผู้ใช้ — เหมือน INPUT_CLASS แต่ไม่มี mt-1 (Select ครอบ div.relative: มาร์จินที่ wrapper ข้างนอกแทน)
|
|
const SELECT_CLASS = "genesis-input block w-full text-base transition";
|
|
|
|
export default function LoginForm({ next }: { next: string | null }) {
|
|
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(next ?? "/");
|
|
} 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-line bg-white p-8">
|
|
{/* brand lockup เดียวกับ nav หน้ารายการ — tile JN น้ำเงินคือแบรนด์มาร์กของระบบ
|
|
(หน้าสรุปแถบน้ำเงิน 2026-09-17 สืบต่อจาก tile นี้) ชื่อแบรนด์เป็น ink ตามกฎ
|
|
"blue เฉพาะของ interactive" ของ Genesis */}
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-primary text-base font-bold text-white">
|
|
JN
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="font-display text-lg font-bold leading-tight text-ink">
|
|
Judge's Note
|
|
</p>
|
|
<p className="mt-0.5 text-xs text-ink-soft">
|
|
ระบบ PDF Annotation สำหรับ e-Court
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h1 className="font-display mt-8 text-subhead text-ink">เข้าสู่ระบบ</h1>
|
|
|
|
<form onSubmit={onSubmit} className="mt-5">
|
|
<div>
|
|
<label htmlFor="username" className="block text-sm font-medium text-ink">
|
|
ชื่อผู้ใช้
|
|
</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="block text-sm font-medium text-ink">
|
|
รหัสผ่าน
|
|
</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="focus-ring mt-6 w-full rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-white transition hover:-translate-y-px hover:bg-primary-hover hover:shadow-btn-glow disabled:pointer-events-none disabled:opacity-50"
|
|
>
|
|
{pending ? "กำลังตรวจสอบ…" : "เข้าสู่ระบบ"}
|
|
</button>
|
|
|
|
{/* รหัสผ่านทดสอบ — รายชื่อเห็นใน dropdown แล้ว เหลือบอกแค่รหัสผ่าน (mock ไม่มีระบบสมัคร)
|
|
กล่อง metadata 8px radius ตาม Genesis (พื้นเทาโปร่งจาง ไม่ใช่การเน้น) */}
|
|
<p className="mt-5 rounded-lg bg-ink/[0.04] px-3 py-2.5 text-xs leading-relaxed text-ink-soft">
|
|
รหัสผ่านทดสอบ:{" "}
|
|
<span className="font-semibold text-ink">Demo1234</span>
|
|
</p>
|
|
|
|
{error && (
|
|
<p role="alert" className="mt-3 text-xs text-error-ink">
|
|
{error}
|
|
</p>
|
|
)}
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|