eCourt-note/components/LoginForm.tsx
Your Name c5c6b65bd2 genesis+worker: design system Genesis ทั้งชุด + patch pdf.worker แก้ fake-space ไทย + ปิด review design/responsive/quality รอบ 2026-09-24 — (1) Genesis: ฟอนต์ General Sans self-host + DM Sans + Sarabun, AppNav 56px backdrop-blur แทน SessionBar, DotGrid หน้า login, palette/รัศมี/เงา/focus ตาม docs/DESIGN.md ครบทุกหน้า (2) postinstall scripts/patch-pdf-worker.mjs ถอด if (scaledDim) รอบ prevTransform ให้อัปเดตทุก glyph — กัน addFakeSpaces แทรก space กลางคำไทยใน PDF ที่ justify ด้วย letter-spacing + test กัน regression (tests/scripts/ + fixture thai_letterspacing.pdf, invariant #6) (3) ฟอร์มอัปโหลด + ช่องกลุ่ม typeId ทางเลือก — normDocumentGroup ที่ utils/external-file.ts เป็น single source ร่วมกับ N8 (4) token fixes: focus ring ปุ่ม pin + แถว bookmark ตามหน้า, ปุ่ม action ใน popup/composer/dialog เป็น rounded-md ทั้งคู่, selection preview เป็น tint ของ primary แทน blue-500 ดิบ, default สี shape/highlight รวมที่ utils/annotation-display, loading animate-pulse + จดข้อยกเว้น (เงา pin, font weights ของ viewer) ลง DESIGN.md (5) responsive: Export PDF max-lg:order-first ให้เห็นบน iPad, แถวบน header แถวเดียวเลื่อนได้บนจอแคบ, re-fit ความกว้างตอน toggle sidebar เฉพาะเมื่อยังไม่ซูมเอง, pin popover flip ขึ้นบนเมื่อใกล้ท้ายหน้า, toast hint ตอนสลับ tool กลางเอกสาร, ป้าย "ชนิด · ผู้ทำ" ของรายการผู้อื่นสำหรับจอสัมผัส (6) คุณภาพ: @layer ครอบ @media กันกฎ touch รั่ยไป desktop (Tailwind v3 ยก @layer ออกจาก media ทิ้ง wrapper — ยืนยันด้วย build จริง + วัดบนจอ), กฎ M5 ครอบ contenteditable กัน Safari zoom ตอนแก้ไข TEXTBOX บน iPad, COMMENT ใช้ box มาตราฐานเดียวตาม pin px จริง (render/scroll target/move-clamp/bookmark), ตัดหัวการ์ดซ้ำกับ h2 หน้ารายการ — พร้อมไฟล์ untracked ที่ postinstall/โค้ดอ้างถึงครบชุด (app/fonts/, AppNav, DotGrid, tests/) กัน fresh clone fail ที่ npm install (คำขอผู้ใช้ 2026-09-24)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 11:46:02 +07:00

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&apos;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>
);
}