import React, { useState, useEffect, useRef } from 'react' import { motion } from 'framer-motion' import { FaUsers, FaComments, FaUserPlus, FaTh, FaExpand, FaHandPaper, FaVolumeMute, FaVolumeUp, FaFile, FaDesktop, FaMicrophone, FaMicrophoneSlash, FaVideo, FaVideoSlash, FaPhone, FaTimes, FaCompress, FaUserFriends, FaLayerGroup, FaWrench, FaFilePdf, FaFileWord, FaFileImage, FaFileAlt, FaBars, } from 'react-icons/fa' import { SignalRService } from '@/services/classroom/signalr' import { WebRTCService } from '@/services/classroom/webrtc' import { ClassroomAttendanceDto, ClassroomChatDto, ClassDocumentDto, ClassroomParticipantDto, ClassroomDto, ClassroomSettingsDto, VideoLayoutDto, } from '@/proxy/classroom/models' import { useStoreState } from '@/store/store' import { KickParticipantModal } from '@/components/classroom/KickParticipantModal' import { useParams } from 'react-router-dom' import { getClassroomAttandances, getClassroomById, getClassroomChats, } from '@/services/classroom.service' import { showDbDateAsIs } from '@/utils/dateUtils' import { useNavigate } from 'react-router-dom' import { endClassroom } from '@/services/classroom.service' import { ROUTES_ENUM } from '@/routes/route.constant' import { Helmet } from 'react-helmet' import { useLocalization } from '@/utils/hooks/useLocalization' import ChatPanel from '@/components/classroom/panels/ChatPanel' import ParticipantsPanel from '@/components/classroom/panels/ParticipantsPanel' import DocumentsPanel from '@/components/classroom/panels/DocumentsPanel' import LayoutPanel from '@/components/classroom/panels/LayoutPanel' import SettingsPanel from '@/components/classroom/panels/SettingsPanel' import { ScreenSharePanel } from '@/components/classroom/panels/ScreenSharePanel' import { ParticipantGrid } from '@/components/classroom/ParticipantGrid' type SidePanelType = | 'chat' | 'participants' | 'documents' | 'handraises' | 'layout' | 'settings' | null const newClassSession: ClassroomDto = { id: '', name: '', teacherId: '', teacherName: '', scheduledStartTime: '', scheduledEndTime: '', actualStartTime: '', actualEndTime: '', participantCount: 0, settingsDto: undefined, } const RoomDetail: React.FC = () => { const params = useParams() const navigate = useNavigate() const { user } = useStoreState((state) => state.auth) const { translate } = useLocalization() const [classSession, setClassSession] = useState(newClassSession) const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [participants, setParticipants] = useState([]) const [localStream, setLocalStream] = useState() const [isAudioEnabled, setIsAudioEnabled] = useState(true) const [isVideoEnabled, setIsVideoEnabled] = useState(true) const [attendanceRecords, setAttendanceRecords] = useState([]) const [chatMessages, setChatMessages] = useState([]) const [currentLayout, setCurrentLayout] = useState({ id: 'grid', name: 'Izgara Görünümü', type: 'grid', description: 'Tüm katılımcılar eşit boyutta görünür', }) const [focusedParticipant, setFocusedParticipant] = useState() const [hasRaisedHand, setHasRaisedHand] = useState(false) const [isAllMuted, setIsAllMuted] = useState(false) const [kickingParticipant, setKickingParticipant] = useState<{ id: string; name: string } | null>( null, ) const [documents, setDocuments] = useState([]) const [isScreenSharing, setIsScreenSharing] = useState(false) const [screenStream, setScreenStream] = useState() const [screenSharer, setScreenSharer] = useState() const [isFullscreen, setIsFullscreen] = useState(false) const [activeSidePanel, setActiveSidePanel] = useState(null) const [newMessage, setNewMessage] = useState('') const [messageMode, setMessageMode] = useState<'public' | 'private' | 'announcement'>('public') const [selectedRecipient, setSelectedRecipient] = useState<{ id: string; name: string } | null>( null, ) const [dragOver, setDragOver] = useState(false) const [participantsActiveTab, setParticipantsActiveTab] = useState<'participants' | 'attendance'>( 'participants', ) const fileInputRef = useRef(null) const messagesEndRef = useRef(null) const [classSettings, setClassSettings] = useState({ allowHandRaise: true, defaultMicrophoneState: 'muted', defaultCameraState: 'on', defaultLayout: 'grid', allowStudentScreenShare: false, allowStudentChat: true, allowPrivateMessages: true, autoMuteNewParticipants: true, }) const signalRServiceRef = useRef() const webRTCServiceRef = useRef() const layouts: VideoLayoutDto[] = [ { id: 'grid', name: 'Izgara Görünümü', type: 'grid', description: 'Tüm katılımcılar eşit boyutta görünür', }, { id: 'sidebar', name: 'Sunum Modu', type: 'sidebar', description: 'Ana konuşmacı büyük, diğerleri yan panelde', }, { id: 'teacher-focus', name: 'Öğretmen Odaklı', type: 'teacher-focus', description: 'Öğretmen tam ekranda görünür, öğrenciler küçük panelde', }, ] const fetchClassAttendances = async () => { if (!params?.id) return const attResult = await getClassroomAttandances(params.id) if (attResult && attResult.data) { setAttendanceRecords(attResult.data) } } const fetchClassChats = async () => { if (!params?.id) return const chatResult = await getClassroomChats(params.id) if (chatResult && chatResult.data) { setChatMessages(chatResult.data || []) } } const fetchClassDetails = async () => { const classEntity = await getClassroomById(params?.id ?? '') if (classEntity) { classEntity.data.scheduledStartTime = showDbDateAsIs(classEntity.data.scheduledStartTime) setClassSession(classEntity.data) } } useEffect(() => { fetchClassDetails() fetchClassChats() fetchClassAttendances() }, []) useEffect(() => { if (classSession.id) { initializeServices() return () => { cleanup() } } }, [classSession.id]) // Apply class settings useEffect(() => { if (classSession?.settingsDto) { setClassSettings(classSession.settingsDto) const selectedLayout = layouts.find((l) => l.id === classSession.settingsDto!.defaultLayout) || layouts[0] setCurrentLayout(selectedLayout) // Apply default audio/video states for new participants if (user.role === 'student') { setIsAudioEnabled(classSession.settingsDto.defaultMicrophoneState === 'unmuted') setIsVideoEnabled(classSession.settingsDto.defaultCameraState === 'on') } } }, [classSession?.settingsDto, user.role]) useEffect(() => { scrollToBottom() }, [chatMessages]) const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) } const initializeServices = async () => { try { // Initialize SignalR signalRServiceRef.current = new SignalRService() await signalRServiceRef.current.start() // Initialize WebRTC webRTCServiceRef.current = new WebRTCService() const stream = await webRTCServiceRef.current.initializeLocalStream() setLocalStream(stream) // Setup WebRTC remote stream handler webRTCServiceRef.current.onRemoteStreamReceived((userId, stream) => { console.log('Received remote stream from:', userId) setParticipants((prev) => prev.map((p) => (p.id === userId ? { ...p, stream } : p))) }) webRTCServiceRef.current.setIceCandidateHandler(async (toUserId, candidate) => { if (signalRServiceRef.current) { await signalRServiceRef.current.sendIceCandidate(classSession.id, toUserId, candidate) } }) signalRServiceRef.current.setOfferReceivedHandler(async (fromUserId, offer) => { if (!webRTCServiceRef.current?.getPeerConnection(fromUserId)) { await webRTCServiceRef.current?.createPeerConnection(fromUserId) } const answer = await webRTCServiceRef.current?.createAnswer(fromUserId, offer) if (answer) { await signalRServiceRef.current?.sendAnswer(classSession.id, fromUserId, answer) } }) signalRServiceRef.current.setAnswerReceivedHandler(async (fromUserId, answer) => { await webRTCServiceRef.current?.handleAnswer(fromUserId, answer) }) signalRServiceRef.current.setIceCandidateReceivedHandler(async (fromUserId, candidate) => { await webRTCServiceRef.current?.addIceCandidate(fromUserId, candidate) }) signalRServiceRef.current.setParticipantJoinHandler( async (userId: string, name: string, isTeacher: boolean) => { if (userId === user.id) return console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`) if (webRTCServiceRef.current) { if (!webRTCServiceRef.current.getPeerConnection(userId)) { await webRTCServiceRef.current.createPeerConnection(userId) } // student → teacher if (user.role === 'student' && isTeacher) { const offer = await webRTCServiceRef.current.createOffer(userId) await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer) } // teacher → student if (user.role === 'teacher' && !isTeacher) { const offer = await webRTCServiceRef.current.createOffer(userId) await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer) } // teacher ↔ teacher if (user.role === 'teacher' && isTeacher) { if (user.id < userId) { const offer = await webRTCServiceRef.current.createOffer(userId) await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer) } } } setParticipants((prev) => { const exists = prev.find((p) => p.id === userId) if (exists) return prev return [ ...prev, { id: userId, name, isTeacher, isAudioMuted: classSettings.autoMuteNewParticipants, isVideoMuted: classSettings.defaultCameraState === 'off', }, ] }) }, ) // 🔑 ExistingParticipants handler signalRServiceRef.current.setExistingParticipantsHandler( async (existing: { userId: string; userName: string; isTeacher: boolean }[]) => { for (const participant of existing) { if (participant.userId === user.id) continue console.log( `Existing participant: ${participant.userName}, isTeacher: ${participant.isTeacher}`, ) if (webRTCServiceRef.current) { if (!webRTCServiceRef.current.getPeerConnection(participant.userId)) { await webRTCServiceRef.current.createPeerConnection(participant.userId) } // 🔑 Eğer ben öğrenci isem, öğretmen için offer gönder if (user.role === 'student' && participant.isTeacher) { const offer = await webRTCServiceRef.current.createOffer(participant.userId) await signalRServiceRef.current?.sendOffer( classSession.id, participant.userId, offer, ) } // 🔑 Eğer ben öğretmensem, öğrenci için offer gönder if (user.role === 'teacher' && !participant.isTeacher) { const offer = await webRTCServiceRef.current.createOffer(participant.userId) await signalRServiceRef.current?.sendOffer( classSession.id, participant.userId, offer, ) } // 🔑 Öğretmen ↔ Öğretmen if (user.role === 'teacher' && participant.isTeacher) { // id’si küçük olan offer göndersin if (user.id < participant.userId) { const offer = await webRTCServiceRef.current.createOffer(participant.userId) await signalRServiceRef.current?.sendOffer( classSession.id, participant.userId, offer, ) } } } setParticipants((prev) => { const exists = prev.find((p) => p.id === participant.userId) if (exists) return prev return [ ...prev, { id: participant.userId, name: participant.userName, isTeacher: participant.isTeacher, isAudioMuted: classSettings.autoMuteNewParticipants, isVideoMuted: classSettings.defaultCameraState === 'off', }, ] }) } }, ) signalRServiceRef.current.setParticipantLeaveHandler((userId) => { console.log(`Participant left: ${userId}`) // peer connection’ı kapat webRTCServiceRef.current?.closePeerConnection(userId) // katılımcıyı state’den sil setParticipants((prev) => prev.filter((p) => p.id !== userId)) }) signalRServiceRef.current.setAttendanceUpdatedHandler((record) => { setAttendanceRecords((prev) => { const existing = prev.find((r) => r.id === record.id) if (existing) { return prev.map((r) => (r.id === record.id ? record : r)) } return [...prev, record] }) }) signalRServiceRef.current.setChatMessageReceivedHandler((message) => { setChatMessages((prev) => [...prev, message]) }) signalRServiceRef.current.setParticipantMutedHandler((userId, isMuted) => { setParticipants((prev) => prev.map((p) => (p.id === userId ? { ...p, isAudioMuted: isMuted } : p)), ) }) // Hand raise events signalRServiceRef.current.setHandRaiseReceivedHandler((studentId) => { setParticipants((prev) => prev.map((p) => (p.id === studentId ? { ...p, isHandRaised: true } : p)), ) }) signalRServiceRef.current.setHandRaiseDismissedHandler((studentId) => { setParticipants((prev) => prev.map((p) => (p.id === studentId ? { ...p, isHandRaised: false } : p)), ) // 👇 kendi state’ini de sıfırla if (studentId === user.id) { setHasRaisedHand(false) } }) // Join the class await signalRServiceRef.current.joinClass( classSession.id, user.id, user.name, user.role === 'teacher', ) } catch (error) { console.error('Failed to initialize services:', error) } } const cleanup = async () => { if (signalRServiceRef.current) { await signalRServiceRef.current.leaveClass(classSession.id) await signalRServiceRef.current.disconnect() } webRTCServiceRef.current?.closeAllConnections() } const handleToggleAudio = () => { setIsAudioEnabled(!isAudioEnabled) webRTCServiceRef.current?.toggleAudio(!isAudioEnabled) } const handleToggleVideo = () => { setIsVideoEnabled(!isVideoEnabled) webRTCServiceRef.current?.toggleVideo(!isVideoEnabled) } const handleLeaveCall = async () => { try { // Eğer teacher ise sınıfı kapat if (user.role === 'teacher' && user.id === classSession.teacherId) { await endClassroom(classSession.id) } // Bağlantıları kapat await cleanup() // Başka sayfaya yönlendir navigate(ROUTES_ENUM.protected.admin.classroom.classes) } catch (err) { console.error('Leave işlemi sırasında hata:', err) navigate(ROUTES_ENUM.protected.admin.classroom.classes) } } const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault() if (newMessage.trim() && signalRServiceRef.current) { if (messageMode === 'private' && selectedRecipient) { await signalRServiceRef.current.sendPrivateMessage( classSession.id, user.id, user.name, newMessage.trim(), selectedRecipient.id, selectedRecipient.name, user.role === 'teacher', ) } else if (messageMode === 'announcement' && user.role === 'teacher') { await signalRServiceRef.current.sendAnnouncement( classSession.id, user.id, user.name, newMessage.trim(), user.role === 'teacher', ) } else { await signalRServiceRef.current.sendChatMessage( classSession.id, user.id, user.name, newMessage.trim(), user.role === 'teacher', ) } setNewMessage('') } } const handleMuteParticipant = async ( participantId: string, isMuted: boolean, isTeacher: boolean, ) => { if (signalRServiceRef.current && user.role === 'teacher') { await signalRServiceRef.current.muteParticipant( classSession.id, participantId, isMuted, isTeacher, ) } } const handleMuteAll = async () => { if (signalRServiceRef.current && user.role === 'teacher') { const newMuteState = !isAllMuted setIsAllMuted(newMuteState) // Mute all participants except teacher for (const participant of participants) { if (!participant.isTeacher) { await signalRServiceRef.current.muteParticipant( classSession.id, participant.id, newMuteState, user.role === 'teacher', ) } } } } const handleRaiseHand = async () => { if ( signalRServiceRef.current && user.role === 'student' && !hasRaisedHand && classSettings.allowHandRaise ) { await signalRServiceRef.current.raiseHand(classSession.id, user.id, user.name) setHasRaisedHand(true) } } const handleKickParticipant = async (participantId: string) => { if (signalRServiceRef.current && user.role === 'teacher') { await signalRServiceRef.current.kickParticipant(classSession.id, participantId) setParticipants((prev) => prev.filter((p) => p.id !== participantId)) // Update attendance record for kicked participant setAttendanceRecords((prev) => prev.map((r) => { if (r.studentId === participantId && !r.leaveTime) { const leaveTime = new Date().toISOString() const join = new Date(r.joinTime) const leave = new Date(leaveTime) const totalDurationMinutes = Math.max( 1, Math.round((leave.getTime() - join.getTime()) / 60000), ) return { ...r, leaveTime, totalDurationMinutes } } return r }), ) } } const handleUploadDocument = async (file: File) => { // In a real app, this would upload to a server const newDoc: ClassDocumentDto = { id: crypto.randomUUID(), name: file.name, url: URL.createObjectURL(file), type: file.type, size: file.size, uploadedAt: new Date().toISOString(), uploadedBy: user.name, } setDocuments((prev) => [...prev, newDoc]) } const handleDeleteDocument = (documentId: string) => { setDocuments((prev) => prev.filter((d) => d.id !== documentId)) } const handleViewDocument = (document: ClassDocumentDto) => { window.open(document.url, '_blank') } const handleStartScreenShare = async () => { try { const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true, }) setScreenStream(stream) setIsScreenSharing(true) setScreenSharer(user.name) // Handle stream end stream.getVideoTracks()[0].onended = () => { handleStopScreenShare() } } catch (error) { console.error('Error starting screen share:', error) } } const handleStopScreenShare = () => { if (screenStream) { screenStream.getTracks().forEach((track) => track.stop()) setScreenStream(undefined) } setIsScreenSharing(false) setScreenSharer(undefined) } const handleLayoutChange = (layout: VideoLayoutDto) => { setCurrentLayout(layout) if (layout.type === 'grid') { setFocusedParticipant(undefined) } } const handleParticipantFocus = (participantId: string | undefined) => { setFocusedParticipant(participantId) } const toggleFullscreen = () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen() setIsFullscreen(true) } else { document.exitFullscreen() setIsFullscreen(false) } } const toggleSidePanel = (panelType: SidePanelType) => { setActiveSidePanel(activeSidePanel === panelType ? null : panelType) } // Demo: Simulate student joining const simulateStudentJoin = async () => { const studentNames = ['Ahmet Yılmaz', 'Fatma Demir', 'Mehmet Kaya', 'Ayşe Özkan', 'Ali Çelik'] const availableNames = studentNames.filter((name) => !participants.some((p) => p.name === name)) if (availableNames.length === 0) { alert('Tüm demo öğrenciler zaten sınıfta!') return } const randomName = availableNames[Math.floor(Math.random() * availableNames.length)] const studentId = crypto.randomUUID() // Guid formatında id üretiliyor // SignalR üzerinden joinClass çağrılıyor await signalRServiceRef.current?.joinClass( classSession.id, studentId, randomName, false, // öğrenci ) } const handleSettingsChange = (newSettings: Partial) => { setClassSettings((prev) => ({ ...prev, ...newSettings })) } const formatTime = (timestamp: string) => { return new Date(timestamp).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit', }) } const formatDuration = (minutes: number) => { const hours = Math.floor(minutes / 60) const mins = minutes % 60 if (hours > 0) { return `${hours}h ${mins}m` } return `${mins}m` } const getTimeSince = (timestamp: string) => { const now = new Date() const time = new Date(timestamp) const diffMinutes = Math.floor((now.getTime() - time.getTime()) / 60000) if (diffMinutes < 1) return 'Az önce' if (diffMinutes < 60) return `${diffMinutes} dakika önce` const hours = Math.floor(diffMinutes / 60) return `${hours} saat önce` } const getFileIcon = (type: string) => { if (type.includes('pdf')) return if ( type.includes('word') || type.includes('doc') || type.includes('presentation') || type.includes('powerpoint') ) return if (type.includes('image')) return return } const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes' const k = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } const handleDrop = (e: React.DragEvent) => { e.preventDefault() setDragOver(false) if (user.role !== 'teacher' || !handleUploadDocument) return const files = Array.from(e.dataTransfer.files) files.forEach((file) => handleUploadDocument(file)) } const handleFileSelect = (e: React.ChangeEvent) => { if (user.role !== 'teacher' || !handleUploadDocument) return const files = Array.from(e.target.files || []) files.forEach((file) => handleUploadDocument(file)) // Reset input if (fileInputRef.current) { fileInputRef.current.value = '' } } const getLayoutIcon = (type: string) => { switch (type) { case 'grid': return case 'speaker': return case 'presentation': return case 'sidebar': return default: return } } const renderSidePanel = () => { if (!activeSidePanel) return null switch (activeSidePanel) { case 'chat': return ( setActiveSidePanel(null)} formatTime={formatTime} /> ) case 'participants': return ( setActiveSidePanel(null)} formatTime={formatTime} formatDuration={formatDuration} /> ) case 'documents': return ( setActiveSidePanel(null)} formatFileSize={formatFileSize} getFileIcon={getFileIcon} /> ) case 'layout': return ( setActiveSidePanel(null)} /> ) case 'settings': return ( setActiveSidePanel(null)} /> ) default: return null } } return ( <>
{/* Main Content Area */}
{/* Left Content Area - Video and Screen Share */}
{/* Video Container - Panel kapalıyken ortalanmış */}
{/* Screen Share Panel */} {(isScreenSharing || screenStream) && (
)} {/* Video Grid */}
{} : handleToggleAudio} onToggleVideo={user.role === 'observer' ? () => {} : handleToggleVideo} onLeaveCall={handleLeaveCall} onMuteParticipant={handleMuteParticipant} layout={currentLayout} focusedParticipant={focusedParticipant} onParticipantFocus={handleParticipantFocus} hasSidePanel={!!activeSidePanel} onKickParticipant={ user.role === 'teacher' ? (participantId) => { const participant = participants.find((p) => p.id === participantId) if (participant) { setKickingParticipant({ id: participant.id, name: participant.name }) } } : undefined } />
{/* Side Panel */} {activeSidePanel && (
{renderSidePanel()}
)}
{/* Bottom Control Bar - Google Meet Style */}
{/* Mobile Layout */}
{/* Left Side - Main Controls */}
{/* Audio Control */} {user.role !== 'observer' && ( )} {/* Video Control */} {user.role !== 'observer' && ( )} {/* Screen Share */} {(user.role === 'teacher' || classSettings.allowStudentScreenShare) && ( )} {/* Hand Raise (Students) */} {user.role === 'student' && classSettings.allowHandRaise && ( )} {/* Leave Call */}
{/* Right Side - Panel Controls */}
{/* Hamburger Menu Modal */} {mobileMenuOpen && ( <> {/* Overlay */}
setMobileMenuOpen(false)} /> {/* Drawer */}
Menü
{user.role === 'teacher' && ( <> )}
)}
{/* Desktop Layout */}
{/* Left Side - Meeting Info */}
{classSession?.name}
{new Date().toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' })}
{/* Center - Main Controls */}
{/* Audio Control */} {user.role !== 'observer' && ( )} {/* Video Control */} {user.role !== 'observer' && ( )} {/* Screen Share */} {(user.role === 'teacher' || classSettings.allowStudentScreenShare) && ( )} {/* Hand Raise (Students) */} {user.role === 'student' && classSettings.allowHandRaise && ( )} {/* Leave Call */}
{/* Right Side - Panel Controls & Participant Count */}
{/* Participant Count */}
{participants.length + 1}
{/* Fullscreen Toggle */} {/* Chat */} {((user.role !== 'observer' && classSettings.allowStudentChat) || user.role === 'teacher') && ( )} {/* Parmak Kaldır (Öğrenci) */} {user.role === 'student' && classSettings.allowHandRaise && ( )} {/* Participants */} {/* Teacher Only Options */} {user.role === 'teacher' && ( <> {/* Documents Button */} {/* Mute All Button */} {/* Add Student Demo Button */} )} {/* Layout Button */} {/* Settings Button */}
{/* Kick Participant Modal */} setKickingParticipant(null)} onConfirm={handleKickParticipant} />
) } export default RoomDetail