เพิ่มตัวเลือก mock user 10 คนที่หน้าแรก + ปรับหน้าแรก + select ลูกศรเองทั่วแอป
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 47s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 3s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s

- utils/user.ts: MOCK_USERS (userId คงที่) + setLocalUser/getStoredLocalUser
  เขียนลง localStorage key เดิม — PDFViewer อ่าน getLocalUser() เหมือนเดิม
- components/UserPicker.tsx: dropdown เลือกผู้ใช้ก่อนเข้า annotate
  (ยังไม่มี identity = default คนแรก, identity สุ่มเดิมแสดงเป็น option ชั่วคราว)
- components/Select.tsx: select ถอดลูกศร native (ชิดขอบ) ใส่ ChevronDown เอง
  ใช้ที่ UserPicker + ตัวกรองรายคน + เลือกหัวข้อ bookmark 2 จุด
- app/page.tsx: header monogram, การ์ดไฟล์ hover, empty state ใหม่

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
DESKTOP-1R2VSQH\Lenovo ThinkPad E490 2026-09-10 18:02:01 +07:00
parent 79c9e2cc41
commit 289d735bf3
6 changed files with 162 additions and 21 deletions

View file

@ -4,6 +4,7 @@ import { FileText } from "lucide-react";
import { db } from "@/db";
import { files } from "@/db/schema";
import UserPicker from "@/components/UserPicker";
export const dynamic = "force-dynamic";
@ -11,29 +12,55 @@ export default async function HomePage() {
const rows = await db.select().from(files).orderBy(asc(files.createdAt));
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="text-2xl font-bold text-primary">Jude&apos;s Note</h1>
<p className="mt-1 text-sm text-slate-500">
ระบบ PDF Annotation สำหรับ e-Court
</p>
<main className="mx-auto max-w-2xl px-4 py-10 sm:px-8">
<header 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>
<h1 className="text-2xl font-bold leading-tight text-primary">
Jude&apos;s Note
</h1>
<p className="text-sm text-slate-500">
ระบบ PDF Annotation สำหรับ e-Court
</p>
</div>
</header>
<UserPicker />
<div className="mt-8 flex items-baseline justify-between">
<h2 className="text-sm font-semibold text-slate-700">เอกสาร</h2>
{rows.length > 0 && (
<span className="text-xs text-slate-400">{rows.length} ไฟล์</span>
)}
</div>
{rows.length === 0 ? (
<p className="mt-8 rounded border border-dashed p-6 text-center text-sm text-slate-500">
ยังไม่มีไฟล์ในระบบ — รัน /sync-db เพื่อ seed ไฟล์ตัวอย่าง
</p>
<div className="mt-3 rounded-xl border border-dashed border-slate-300 bg-white/60 p-10 text-center">
<FileText className="mx-auto h-8 w-8 text-slate-300" />
<p className="mt-3 text-sm font-medium text-slate-500">
ยังไม่มีไฟล์ในระบบ
</p>
<p className="mt-1 text-xs text-slate-400">
รัน /sync-db เพื่อ seed ไฟล์ตัวอย่าง
</p>
</div>
) : (
<ul className="mt-6 space-y-2">
<ul className="mt-3 space-y-3">
{rows.map((f) => (
<li
key={f.id}
className="flex items-center gap-3 rounded-lg border bg-white p-4 shadow-sm"
className="group flex items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-primary/30 hover:shadow-md"
>
<FileText className="h-5 w-5 shrink-0 text-slate-400" />
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
<FileText className="h-5 w-5 text-primary" />
</div>
<span className="min-w-0 flex-1">
<span className="block break-all line-clamp-2 text-sm font-medium">
<span className="block break-all line-clamp-2 text-sm font-medium text-slate-900">
{f.title}
</span>
<span className="block text-xs text-slate-400">
<span className="mt-0.5 block text-xs text-slate-400">
{new Date(f.createdAt).toLocaleString("th-TH")}
</span>
</span>

19
components/Select.tsx Normal file
View file

@ -0,0 +1,19 @@
"use client";
// <select> ที่ถอดลูกศร native ของ browser ออก (วาดชิดขอบและขยับด้วย padding ไม่ได้)
// แล้วใส่ลูกศรเอง — ใช้เหมือน <select> ปกติทุกอย่าง (props/children)
// className ที่ส่งเข้าไปใช้กับตัว <select> โดย append `appearance-none pr-7` ให้
import type { SelectHTMLAttributes } from "react";
import { ChevronDown } from "lucide-react";
export default function Select({
className = "",
...props
}: SelectHTMLAttributes<HTMLSelectElement>) {
return (
<div className="relative">
<select {...props} className={`appearance-none pr-7 ${className}`} />
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" />
</div>
);
}

66
components/UserPicker.tsx Normal file
View file

@ -0,0 +1,66 @@
"use client";
// เลือก mock user ที่หน้าแรกก่อนเข้า annotate — เขียนลง localStorage key เดิม
// ที่ getLocalUser() อ่าน (PDFViewer ไม่ต้องแก้อะไร)
import { useEffect, useState } from "react";
import Select from "./Select";
import {
MOCK_USERS,
getStoredLocalUser,
setLocalUser,
type LocalUser,
} from "@/utils/user";
export default function UserPicker() {
const [current, setCurrent] = useState<LocalUser | null>(null);
// ต้องรันใน effect เพราะ SSR ไม่มี localStorage — ก่อน mount แสดงคนแรกไว้ก่อน
useEffect(() => {
const stored = getStoredLocalUser();
if (!stored) {
// ยังไม่เคยมี identity → เริ่มด้วย mock user คนแรก
setLocalUser(MOCK_USERS[0]);
setCurrent(MOCK_USERS[0]);
return;
}
setCurrent(stored);
}, []);
// identity สุ่มเดิมไม่อยู่ในรายการ mock → แสดงเป็น option ชั่วคราวให้เห็นว่าปัจจุบันคือใคร
const isMock = current !== null && MOCK_USERS.some((u) => u.userId === current.userId);
const select = (userId: string) => {
const user = MOCK_USERS.find((u) => u.userId === userId);
if (!user) return;
setLocalUser(user);
setCurrent(user);
};
return (
<section className="mt-6 rounded-lg border bg-white p-4 shadow-sm">
<h2 className="text-sm font-semibold text-primary">เลือกผู้ใช้ (mock)</h2>
<p className="mt-0.5 text-xs text-slate-400">
annotation ทั้งหมดจะบันทึกในชื่อผู้ใช้ที่เลือก
</p>
<Select
value={current ? (isMock ? current.userId : "") : MOCK_USERS[0].userId}
onChange={(e) => {
if (e.target.value) select(e.target.value);
}}
className="mt-3 block w-full rounded-md border bg-white py-2 pl-3 text-sm"
>
{current && !isMock && (
<option value="" disabled>
{current.userName} (สุ่มอัตโนมัติ)
</option>
)}
{MOCK_USERS.map((u) => (
<option key={u.userId} value={u.userId}>
{u.userName}
</option>
))}
</Select>
</section>
);
}

View file

@ -109,6 +109,7 @@ import InkLayer, { type InkRedaction, type InkShape, type InkStroke } from "./In
import TextboxStampLayer, { type InkStamp, type InkTextbox } from "./TextboxStampLayer";
import AnnotationSelectionLayer from "./AnnotationSelectionLayer";
import AnnotationSidebar from "../sidebar/AnnotationSidebar";
import Select from "../Select";
import {
ColorSwatches,
HIGHLIGHT_STYLE_BTNS,
@ -2634,7 +2635,7 @@ export default function PDFViewer({
className="mt-1.5 w-full resize-none rounded-md border border-slate-300 px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-slate-400"
/>
<p className="mt-1.5 text-[11px] text-slate-400">วางที่</p>
<select
<Select
value={bookmarkComposer.parentId ?? ""}
onChange={(e) =>
setBookmarkComposer({
@ -2654,7 +2655,7 @@ export default function PDFViewer({
ใต้ &quot;{h.title}&quot;
</option>
))}
</select>
</Select>
{bookmarkError && (
<p className="mt-1.5 rounded bg-red-50 px-2 py-1 text-[11px] text-red-600">
{bookmarkError}

View file

@ -28,6 +28,8 @@ import {
X,
} from "lucide-react";
import Select from "../Select";
import {
listVersions,
restoreVersion,
@ -359,7 +361,7 @@ export default function AnnotationSidebar({
className="mt-1 w-full resize-none rounded border border-slate-300 p-1.5 text-[11px] focus:outline-none focus:ring-1 focus:ring-slate-400"
/>
<p className="mt-1 text-[10px] text-slate-400">วางที่</p>
<select
<Select
value={eb.parentId ?? ""}
onChange={(e) =>
setEditBookmark({ ...eb, parentId: e.target.value || null })
@ -373,7 +375,7 @@ export default function AnnotationSidebar({
ใต้ &quot;{h.title}&quot;
</option>
))}
</select>
</Select>
<div className="mt-1.5 flex justify-end gap-1.5 text-[11px]">
<button
type="button"
@ -622,7 +624,7 @@ export default function AnnotationSidebar({
<div className="flex h-full flex-col">
{/* F4: กรองแสดง annotation ตามผู้เขียน (viewer + sidebar พร้อมกัน) */}
<div className="shrink-0 border-b border-slate-200 p-2">
<select
<Select
aria-label="กรอง annotation ตามผู้เขียน"
className="w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-xs text-slate-600 focus:outline-none focus:ring-2 focus:ring-slate-400"
value={userFilter}
@ -635,7 +637,7 @@ export default function AnnotationSidebar({
เฉพาะ {u.userName}
</option>
))}
</select>
</Select>
</div>
{/* F14/D1: ค้นหาในหมายเหตุทั้งไฟล์ — คลิกผลลัพธ์ = กระโดดไปตำแหน่ง + เลือก */}

View file

@ -5,7 +5,27 @@ export type LocalUser = { userId: string; userName: string };
const STORAGE_KEY = "judes-note:user";
export function getLocalUser(): LocalUser {
// mock users สำหรับเลือกที่หน้าแรก (demo ไม่มีระบบ auth) — userId คงที่
// เพื่อให้กรองรายคนและสิทธิ์ "แก้ไขได้เฉพาะของตัวเอง" ทำงานสม่ำเสมอข้าม refresh
export const MOCK_USERS: LocalUser[] = [
{ userId: "mock-user-01", userName: "ผู้พิพากษา สมชาย" },
{ userId: "mock-user-02", userName: "ผู้พิพากษา สมหญิง" },
{ userId: "mock-user-03", userName: "อัยการ วิชัย" },
{ userId: "mock-user-04", userName: "เลขานุการศาล ปิยะ" },
{ userId: "mock-user-05", userName: "ทนายโจทก์ ณัฐ" },
{ userId: "mock-user-06", userName: "ทนายจำเลย อรุณี" },
{ userId: "mock-user-07", userName: "ผู้ช่วยผู้พิพากษา กันต์" },
{ userId: "mock-user-08", userName: "พนักงานสอบสวน เดชา" },
{ userId: "mock-user-09", userName: "ผู้เชี่ยวชาญ มานะ" },
{ userId: "mock-user-10", userName: "ผู้ช่วยเลขานุการ ฝ้าย" },
];
export function setLocalUser(user: LocalUser): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(user));
}
// อ่าน identity ที่เก็บไว้โดยไม่สร้างใหม่ — ใช้แสดงค่าปัจจุบันในหน้าเลือกผู้ใช้
export function getStoredLocalUser(): LocalUser | null {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
try {
@ -14,9 +34,15 @@ export function getLocalUser(): LocalUser {
return parsed;
}
} catch {
// ข้อมูลเก่าเสียหาย → สร้างใหม่ต่อด้านล่าง
// ข้อมูลเก่าเสียหาย → ถือว่าไม่มีข้อมูล
}
}
return null;
}
export function getLocalUser(): LocalUser {
const stored = getStoredLocalUser();
if (stored) return stored;
const userId = crypto.randomUUID();
const userName = `ผู้ใช้ ${userId.slice(0, 4).toUpperCase()}`;