diff --git a/management/src/components/ExamGridProctor.tsx b/management/src/components/ExamGridProctor.tsx index dc7257a..147bc1e 100644 --- a/management/src/components/ExamGridProctor.tsx +++ b/management/src/components/ExamGridProctor.tsx @@ -20,6 +20,17 @@ export const ExamGridProctor: React.FC = ({ const [searchQuery, setSearchQuery] = useState(''); const [onlyOnline, setOnlyOnline] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); + const [zoomedStudent, setZoomedStudent] = useState(null); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setZoomedStudent(null); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); const wsRef = useRef(null); const subscribedRef = useRef>(new Set()); @@ -31,9 +42,13 @@ export const ExamGridProctor: React.FC = ({ ); const onlineKey = useMemo(() => onlineIds.join(','), [onlineIds]); + const onlineIdsRef = useRef(onlineIds); + onlineIdsRef.current = onlineIds; + const syncSubscriptions = (ws: WebSocket) => { if (ws.readyState !== WebSocket.OPEN) return; - const target = new Set(onlineIds); + const currentOnlineIds = onlineIdsRef.current; + const target = new Set(currentOnlineIds); const prev = subscribedRef.current; prev.forEach((id) => { @@ -42,7 +57,7 @@ export const ExamGridProctor: React.FC = ({ prev.delete(id); } }); - onlineIds.forEach((id) => { + currentOnlineIds.forEach((id) => { if (!prev.has(id)) { ws.send(JSON.stringify({ event: 'teacher:subscribe', data: { studentId: id, mode: 'grid' } })); prev.add(id); @@ -321,7 +336,14 @@ export const ExamGridProctor: React.FC = ({
{isOnline ? ( -
+
{ + e.stopPropagation(); + setZoomedStudent(s); + }} + style={{ cursor: 'zoom-in' }} + > {screenFrame ? ( = ({
)}
+ + {zoomedStudent && ( +
setZoomedStudent(null)} + style={{ + position: 'fixed', + top: 0, + left: 0, + width: '100vw', + height: '100vh', + backgroundColor: 'rgba(10, 15, 30, 0.85)', + backdropFilter: 'blur(8px)', + zIndex: 9999, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '2rem', + animation: 'fadeIn 0.2s ease-out', + }} + > +
e.stopPropagation()} + style={{ + width: '100%', + maxWidth: '1200px', + backgroundColor: '#161d2a', + border: '1px solid rgba(255, 255, 255, 0.1)', + borderRadius: '16px', + boxShadow: '0 20px 40px rgba(0, 0, 0, 0.5)', + display: 'flex', + flexDirection: 'column', + maxHeight: '90vh', + overflow: 'hidden', + }} + > + {/* Modal Header */} +
+
+ +

+ {zoomedStudent.fullName} +

+ + {zoomedStudent.studentCode} + +
+ +
+ + {/* Modal Body */} +
+ {screenFrames[zoomedStudent.studentRkId] ? ( +
+ Zoomed Screen + {showWebcamOverlay && webcamFrames[zoomedStudent.studentRkId] && ( +
+ Zoomed Webcam +
+ )} +
+ ) : ( +
+
+ Đang tải màn hình... +
+ )} +
+
+
+ )} + +
); };