From f1de8cb6274e1c382d70a9fa3b99e0dc95439dab Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 21 May 2026 09:15:09 +0700 Subject: [PATCH] update build UI AU --- .../pages/main/components/face-bracket.tsx | 9 +- .../main/components/tab-features/index.tsx | 10 +- .../client/src/pages/main/index.tsx | 119 +++++++++++++----- 3 files changed, 104 insertions(+), 34 deletions(-) diff --git a/TrackingToolWebAU/client/src/pages/main/components/face-bracket.tsx b/TrackingToolWebAU/client/src/pages/main/components/face-bracket.tsx index 28853d0..28c82f9 100644 --- a/TrackingToolWebAU/client/src/pages/main/components/face-bracket.tsx +++ b/TrackingToolWebAU/client/src/pages/main/components/face-bracket.tsx @@ -40,8 +40,13 @@ export default function FaceBracket({ box, transform, progress }: Props) { return null; } - const armPx = Math.max(14, Math.min(40, width * 1000 * 0.18)); - const cornerSize = { width: armPx, height: armPx } as const; + // Arm length tính theo % cạnh bracket để scale đúng trên mọi kích thước Card + // (mobile ~350px, desktop ~1000px). Sàn tối thiểu 10px để không tàng hình + // khi mặt detect rất nhỏ; trần 36px để khi mặt to không bị quá dày. + const cornerSize = { + width: "min(36px, max(10px, 22%))", + height: "min(36px, max(10px, 22%))", + } as const; const cornerBase = "absolute border-emerald-400/90 transition-opacity duration-150"; diff --git a/TrackingToolWebAU/client/src/pages/main/components/tab-features/index.tsx b/TrackingToolWebAU/client/src/pages/main/components/tab-features/index.tsx index f5808af..e93a7c3 100644 --- a/TrackingToolWebAU/client/src/pages/main/components/tab-features/index.tsx +++ b/TrackingToolWebAU/client/src/pages/main/components/tab-features/index.tsx @@ -113,10 +113,14 @@ export default function TabFeatures({ inline = false }: { inline?: boolean }) { // Stable-face auto trigger: Main bumps autoCheckinTick when a face has been // present for 2s, and we fire the same checkin path used by the manual - // button. Skip the initial 0 tick on mount. - const lastHandledTick = useRef(0); + // button. Khởi tạo null để mỗi lần remount (vd: resize đổi layout) không + // bắn nhầm tick cũ — lần chạy đầu chỉ "ghi nhớ" tick hiện tại rồi return. + const lastHandledTick = useRef(null); useEffect(() => { - if (autoCheckinTick === 0) return; + if (lastHandledTick.current === null) { + lastHandledTick.current = autoCheckinTick; + return; + } if (autoCheckinTick === lastHandledTick.current) return; lastHandledTick.current = autoCheckinTick; if (loading) return; diff --git a/TrackingToolWebAU/client/src/pages/main/index.tsx b/TrackingToolWebAU/client/src/pages/main/index.tsx index 43bb5d3..9800cc2 100644 --- a/TrackingToolWebAU/client/src/pages/main/index.tsx +++ b/TrackingToolWebAU/client/src/pages/main/index.tsx @@ -1,5 +1,4 @@ /* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable @typescript-eslint/no-explicit-any */ "use client"; import { Button } from "@/components/ui/button"; @@ -17,9 +16,28 @@ import RightSlidebar from "./components/right-slidebar"; import TabFeatures from "./components/tab-features"; import { LogList } from "./components/tab-log"; +const DESKTOP_MQ = "(min-width: 1024px)"; +const matchDesktop = () => + typeof window !== "undefined" + ? window.matchMedia(DESKTOP_MQ).matches + : true; + export default function Main() { - const [isSidebarOpen, setIsSidebarOpen] = useState(true); + const [isDesktop, setIsDesktop] = useState(matchDesktop); + const [isSidebarOpen, setIsSidebarOpen] = useState(matchDesktop); const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(false); + + useEffect(() => { + if (typeof window === "undefined") return; + const mq = window.matchMedia(DESKTOP_MQ); + const handler = (e: MediaQueryListEvent) => { + setIsDesktop(e.matches); + // Resize xuống tablet/mobile thì đóng luôn drawer logs (tắt auto-open). + if (!e.matches) setIsSidebarOpen(false); + }; + mq.addEventListener("change", handler); + return () => mq.removeEventListener("change", handler); + }, []); // const { currentUser, setCurrentUser } = useUserStore(); const { @@ -36,6 +54,37 @@ export default function Main() { const videoRef = useRef(null); const canvasRef = useRef(null); + const cardRef = useRef(null); + + // Kích thước thực của khung hiển thị video bên trong Card sau khi + // letterbox theo aspect-ratio nguồn (1280x720 = 16:9). FaceBracket dùng % + // của container này nên bám đúng vùng video, không lệch ra vùng đen. + const [videoFrame, setVideoFrame] = useState<{ w: number; h: number }>({ + w: 0, + h: 0, + }); + + useEffect(() => { + const el = cardRef.current; + if (!el) return; + const VIDEO_ASPECT = 16 / 9; + const ro = new ResizeObserver(([entry]) => { + const { width, height } = entry.contentRect; + if (!width || !height) return; + const cardAspect = width / height; + let w: number, h: number; + if (cardAspect >= VIDEO_ASPECT) { + h = height; + w = height * VIDEO_ASPECT; + } else { + w = width; + h = width / VIDEO_ASPECT; + } + setVideoFrame({ w: Math.round(w), h: Math.round(h) }); + }); + ro.observe(el); + return () => ro.disconnect(); + }, []); const onStableFace = useCallback(() => { // Fire the same path as the "Điểm Danh Ngay" button via the store trigger, @@ -126,31 +175,42 @@ export default function Main() { )} >
- {/* Mobile/tablet: Buttons at top */} -
- -
+ {/* Mobile/tablet: Buttons at top — conditional render để tránh + mount 2 instance TabFeatures cùng lúc (duplicate auto-check). */} + {!isDesktop && } {/* Video Feed */} -