/* eslint-disable react-hooks/exhaustive-deps */ "use client"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { useFaceZoom } from "@/lib/use-face-zoom"; import { capture, cn } from "@/lib/utils"; import useAppStore from "@/stores/use-app-store"; import { AnimatePresence, motion } from "framer-motion"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import CountDown from "./components/count-down"; import FaceBracket from "./components/face-bracket"; import LeftSlidebar from "./components/left-slidebar"; 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 [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 { setCanvasRef, setVideoRef, setFaceZoomTransform, isAutoChecking, bumpAutoCheckinTick, } = useAppStore(); const { isCountDown, setCaptureRegisterImage, setIsCountDown } = useAppStore(); const [flashKey, setFlashKey] = useState(0); 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, // and play a camera-shutter flash to acknowledge the capture. setFlashKey((k) => k + 1); bumpAutoCheckinTick(); }, [bumpAutoCheckinTick]); const { transform: faceZoom, box: faceBox, stabilityProgress: faceStability, } = useFaceZoom(videoRef, { enabled: true, intervalMs: 110, targetFaceRatio: 0.6, minScale: 1.35, maxScale: 4, followSpeed: 0.11, detectionSmoothing: 0.3, detectorInputSize: 416, detectorScoreThreshold: 0.3, stabilityEnabled: isAutoChecking, stableMs: 2000, onStableFace, }); // Initialize camera useEffect(() => { const initCamera = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 1280, height: 720, facingMode: "user" }, }); if (videoRef.current) { videoRef.current.srcObject = stream; } } catch (err) { console.error("Không thể truy cập camera:", err); } }; initCamera(); return () => { if (videoRef.current?.srcObject) { const stream = videoRef.current?.srcObject as MediaStream; stream.getTracks().forEach((track) => track.stop()); } }; }, []); useEffect(() => { setCanvasRef(canvasRef); setVideoRef(videoRef); }, [videoRef, canvasRef]); // Keep store in sync with the live zoom so capture() can crop to the // visible focused region instead of the full frame. useEffect(() => { setFaceZoomTransform(faceZoom); }, [faceZoom, setFaceZoomTransform]); return (
setIsLeftSidebarOpen(false)} /> {/* Mobile/tablet backdrop */} {(isLeftSidebarOpen || isSidebarOpen) && (
{ setIsLeftSidebarOpen(false); setIsSidebarOpen(false); }} /> )}
{/* Mobile/tablet: Buttons at top — conditional render để tránh mount 2 instance TabFeatures cùng lúc (duplicate auto-check). */} {!isDesktop && } {/* Video Feed */}
{/* Khung video centered theo aspect nguồn — đặt video + bracket chung trong inner container để toạ độ % của bracket bám đúng vùng video, không tràn ra letterbox đen. */}
{/* Camera shutter flash — keyed so each fire replays the animation. */} {flashKey > 0 && ( )} {/* {currentUser && ( setCurrentUser(null)} key="user-card" initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.25 }} className="absolute top-4 left-4 right-4" >
{currentUser.name.charAt(0)}

{currentUser.name}

{currentUser.email}

)}
*/} {isCountDown && ( { const data = await capture(videoRef, canvasRef); setCaptureRegisterImage(data); setIsCountDown(false); }} /> )} {/* Desktop only: buttons overlaid at bottom of video */} {isDesktop && }
{/* Mobile/tablet: Log at bottom */} {!isDesktop && (
)}
setIsSidebarOpen(false)} /> {/* Hidden Canvas for Capture */}
); }