update build UI AU

This commit is contained in:
Admin 2026-05-21 09:15:09 +07:00
parent b50dc96ce2
commit f1de8cb627
3 changed files with 104 additions and 34 deletions

View File

@ -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";

View File

@ -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<number | null>(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;

View File

@ -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<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const cardRef = useRef<HTMLDivElement>(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() {
)}
>
<div className="h-full flex flex-col p-2 sm:p-4 lg:p-6 gap-2 lg:gap-0">
{/* Mobile/tablet: Buttons at top */}
<div className="lg:hidden">
<TabFeatures inline />
</div>
{/* Mobile/tablet: Buttons at top conditional render đ tránh
mount 2 instance TabFeatures cùng lúc (duplicate auto-check). */}
{!isDesktop && <TabFeatures inline />}
{/* Video Feed */}
<Card className="flex-1 min-h-0 overflow-hidden bg-black relative group">
<div ref={cardRef} className="absolute inset-0">
{/* Khung video centered theo aspect ngun đt video + bracket
chung trong inner container đ toạ đ % của bracket bám
đúng vùng video, không tràn ra letterbox đen. */}
<div
className="absolute inset-0 m-auto"
style={{
width: videoFrame.w || "100%",
height: videoFrame.h || "100%",
}}
>
<video
ref={videoRef}
autoPlay
playsInline
className="w-full h-full object-contain lg:object-cover will-change-transform"
className="w-full h-full object-cover will-change-transform"
style={{
transformOrigin: "0 0",
transform: `translate3d(${faceZoom.translateX}%, ${faceZoom.translateY}%, 0) scale(${faceZoom.scale})`,
backfaceVisibility: "hidden",
}}
/>
<div className="absolute inset-0 border-4 border-blue-500/30 pointer-events-none" />
<FaceBracket
box={faceBox}
transform={faceZoom}
progress={isAutoChecking ? faceStability : 0}
/>
</div>
<div className="absolute inset-0 border-4 border-blue-500/30 pointer-events-none" />
{/* Camera shutter flash — keyed so each fire replays the animation. */}
<AnimatePresence>
@ -232,15 +292,16 @@ export default function Main() {
</Button>
{/* Desktop only: buttons overlaid at bottom of video */}
<div className="hidden lg:block">
<TabFeatures />
{isDesktop && <TabFeatures />}
</div>
</Card>
{/* Mobile/tablet: Log at bottom */}
<div className="lg:hidden h-44 border rounded-lg bg-white overflow-hidden">
{!isDesktop && (
<div className="h-44 border rounded-lg bg-white overflow-hidden">
<LogList />
</div>
)}
</div>
</div>