2026-07-06 09:44:26 +00:00
|
|
|
|
import { useState, useEffect, useRef, type FormEvent } from 'react'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
import { motion } from 'framer-motion'
|
|
|
|
|
|
import {
|
|
|
|
|
|
FaComments,
|
|
|
|
|
|
FaExpand,
|
|
|
|
|
|
FaHandPaper,
|
|
|
|
|
|
FaVolumeMute,
|
|
|
|
|
|
FaVolumeUp,
|
|
|
|
|
|
FaFile,
|
|
|
|
|
|
FaDesktop,
|
|
|
|
|
|
FaMicrophone,
|
|
|
|
|
|
FaMicrophoneSlash,
|
|
|
|
|
|
FaVideo,
|
|
|
|
|
|
FaVideoSlash,
|
|
|
|
|
|
FaPhone,
|
|
|
|
|
|
FaTimes,
|
|
|
|
|
|
FaCompress,
|
|
|
|
|
|
FaUserFriends,
|
|
|
|
|
|
FaLayerGroup,
|
|
|
|
|
|
FaFilePdf,
|
|
|
|
|
|
FaFileWord,
|
|
|
|
|
|
FaFileImage,
|
|
|
|
|
|
FaFileAlt,
|
|
|
|
|
|
FaBars,
|
|
|
|
|
|
} from 'react-icons/fa'
|
|
|
|
|
|
import { SignalRService } from '@/services/videoroom/signalr'
|
|
|
|
|
|
import { WebRTCService } from '@/services/videoroom/webrtc'
|
|
|
|
|
|
import { useStoreState } from '@/store/store'
|
|
|
|
|
|
import { KickParticipantModal } from '@/views/admin/videoroom/KickParticipantModal'
|
|
|
|
|
|
import {
|
|
|
|
|
|
getVideoroomAttandances,
|
|
|
|
|
|
getVideoroomById,
|
|
|
|
|
|
getVideoroomChats,
|
2026-07-06 09:44:26 +00:00
|
|
|
|
endVideoroom
|
2026-05-08 05:34:29 +00:00
|
|
|
|
} from '@/services/videoroom.service'
|
|
|
|
|
|
import { showDbDateAsIs } from '@/utils/dateUtils'
|
2026-07-06 09:44:26 +00:00
|
|
|
|
import { useNavigate, useParams } from 'react-router-dom'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
|
|
|
|
import { Helmet } from 'react-helmet'
|
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
|
import ChatPanel from '@/views/admin/videoroom/ChatPanel'
|
|
|
|
|
|
import ParticipantsPanel from '@/views/admin/videoroom/ParticipantsPanel'
|
|
|
|
|
|
import DocumentsPanel from '@/views/admin/videoroom/DocumentsPanel'
|
|
|
|
|
|
import LayoutPanel from '@/views/admin/videoroom/LayoutPanel'
|
|
|
|
|
|
import { ScreenSharePanel } from '@/views/admin/videoroom/ScreenSharePanel'
|
|
|
|
|
|
import { RoomParticipant } from '@/views/admin/videoroom/RoomParticipant'
|
|
|
|
|
|
import toast from '@/components/ui/toast/toast'
|
|
|
|
|
|
import Notification from '@/components/ui/Notification'
|
2026-06-25 22:55:44 +00:00
|
|
|
|
import { Button } from '@/components/ui'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
import {
|
|
|
|
|
|
VideoroomDocumentDto,
|
|
|
|
|
|
VideoroomAttendanceDto,
|
|
|
|
|
|
VideoroomChatDto,
|
|
|
|
|
|
VideoroomDto,
|
|
|
|
|
|
VideoroomParticipantDto,
|
|
|
|
|
|
VideoroomSettingsDto,
|
|
|
|
|
|
MessageType,
|
|
|
|
|
|
VideoroomLayoutDto,
|
|
|
|
|
|
} from '@/proxy/videoroom/models'
|
|
|
|
|
|
|
|
|
|
|
|
type SidePanelType =
|
|
|
|
|
|
| 'chat'
|
|
|
|
|
|
| 'participants'
|
|
|
|
|
|
| 'documents'
|
|
|
|
|
|
| 'handraises'
|
|
|
|
|
|
| 'layout'
|
|
|
|
|
|
| 'settings'
|
|
|
|
|
|
| null
|
|
|
|
|
|
|
|
|
|
|
|
const newClassSession: VideoroomDto = {
|
|
|
|
|
|
id: '',
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
teacherId: '',
|
|
|
|
|
|
teacherName: '',
|
|
|
|
|
|
scheduledStartTime: '',
|
|
|
|
|
|
scheduledEndTime: '',
|
|
|
|
|
|
actualStartTime: '',
|
|
|
|
|
|
actualEndTime: '',
|
|
|
|
|
|
participantCount: 0,
|
|
|
|
|
|
settingsDto: undefined,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 09:44:26 +00:00
|
|
|
|
const RoomDetail = () => {
|
2026-05-08 05:34:29 +00:00
|
|
|
|
const handleDismissHandRaise = async (participantId: string) => {
|
|
|
|
|
|
if (signalRServiceRef.current && user.role === 'teacher') {
|
|
|
|
|
|
await signalRServiceRef.current.dismissHandRaise(classSession.id, participantId)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const params = useParams()
|
|
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
const { user } = useStoreState((state) => state.auth)
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
|
|
|
|
|
|
const [classSession, setClassSession] = useState<VideoroomDto>(newClassSession)
|
|
|
|
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
|
|
|
|
const [participants, setParticipants] = useState<VideoroomParticipantDto[]>([])
|
|
|
|
|
|
const [localStream, setLocalStream] = useState<MediaStream | null>(null)
|
|
|
|
|
|
const [isAudioEnabled, setIsAudioEnabled] = useState(true)
|
|
|
|
|
|
const [isVideoEnabled, setIsVideoEnabled] = useState(true)
|
|
|
|
|
|
const [attendanceRecords, setAttendanceRecords] = useState<VideoroomAttendanceDto[]>([])
|
|
|
|
|
|
const [chatMessages, setChatMessages] = useState<VideoroomChatDto[]>([])
|
|
|
|
|
|
const [currentLayout, setCurrentLayout] = useState<VideoroomLayoutDto | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const [focusedParticipant, setFocusedParticipant] = useState<string>()
|
|
|
|
|
|
const [hasRaisedHand, setHasRaisedHand] = useState(false)
|
|
|
|
|
|
const [isAllMuted, setIsAllMuted] = useState(false)
|
|
|
|
|
|
const [kickingParticipant, setKickingParticipant] = useState<{ id: string; name: string } | null>(
|
|
|
|
|
|
null,
|
|
|
|
|
|
)
|
|
|
|
|
|
const [documents, setDocuments] = useState<VideoroomDocumentDto[]>([])
|
|
|
|
|
|
const [isScreenSharing, setIsScreenSharing] = useState(false)
|
|
|
|
|
|
const [screenStream, setScreenStream] = useState<MediaStream>()
|
|
|
|
|
|
const [screenSharer, setScreenSharer] = useState<string>()
|
|
|
|
|
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
|
|
|
|
const [activeSidePanel, setActiveSidePanel] = useState<SidePanelType>(null)
|
|
|
|
|
|
const [newMessage, setNewMessage] = useState('')
|
|
|
|
|
|
const [messageMode, setMessageMode] = useState<MessageType>('public')
|
|
|
|
|
|
const [selectedRecipient, setSelectedRecipient] = useState<{ id: string; name: string } | null>(
|
|
|
|
|
|
null,
|
|
|
|
|
|
)
|
|
|
|
|
|
const raisedHandsCount = participants.filter((p) => p.isHandRaised).length
|
|
|
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
const [classSettings, setClassSettings] = useState<VideoroomSettingsDto>({
|
|
|
|
|
|
allowHandRaise: true,
|
|
|
|
|
|
defaultMicrophoneState: 'muted',
|
|
|
|
|
|
defaultCameraState: 'on',
|
|
|
|
|
|
defaultLayout: 'grid',
|
|
|
|
|
|
allowStudentScreenShare: false,
|
|
|
|
|
|
allowStudentChat: true,
|
|
|
|
|
|
allowPrivateMessages: true,
|
|
|
|
|
|
autoMuteNewParticipants: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const signalRServiceRef = useRef<SignalRService>()
|
|
|
|
|
|
const webRTCServiceRef = useRef<WebRTCService>()
|
|
|
|
|
|
const [teacherDisconnected, setTeacherDisconnected] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
const layouts: VideoroomLayoutDto[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
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 getVideoroomAttandances(params.id)
|
|
|
|
|
|
if (attResult && attResult.data) {
|
|
|
|
|
|
setAttendanceRecords(attResult.data)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fetchClassChats = async () => {
|
|
|
|
|
|
if (!params?.id) return
|
|
|
|
|
|
const chatResult = await getVideoroomChats(params.id)
|
|
|
|
|
|
if (chatResult && chatResult.data) {
|
|
|
|
|
|
setChatMessages(chatResult.data || [])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fetchClassDetails = async () => {
|
|
|
|
|
|
const classEntity = await getVideoroomById(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()
|
|
|
|
|
|
|
|
|
|
|
|
const micEnabled = classSession.settingsDto?.defaultMicrophoneState === 'unmuted'
|
|
|
|
|
|
const camEnabled = classSession.settingsDto?.defaultCameraState === 'on'
|
|
|
|
|
|
|
|
|
|
|
|
// WebRTC başlat
|
|
|
|
|
|
webRTCServiceRef.current = new WebRTCService()
|
|
|
|
|
|
webRTCServiceRef.current.setSignalRService(signalRServiceRef.current, classSession.id)
|
|
|
|
|
|
|
|
|
|
|
|
const stream = await webRTCServiceRef.current.initializeLocalStream(micEnabled, camEnabled)
|
|
|
|
|
|
if (stream) {
|
|
|
|
|
|
setLocalStream(stream)
|
|
|
|
|
|
}
|
|
|
|
|
|
setIsAudioEnabled(micEnabled)
|
|
|
|
|
|
setIsVideoEnabled(camEnabled)
|
|
|
|
|
|
|
|
|
|
|
|
// Setup WebRTC remote stream handler
|
|
|
|
|
|
webRTCServiceRef.current.onRemoteStreamReceived((userId, stream) => {
|
|
|
|
|
|
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)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 🔑 Yeni katılan birini gördüğünde
|
|
|
|
|
|
signalRServiceRef.current.setParticipantJoinHandler(
|
|
|
|
|
|
async (remoteUserId: string, name: string, isTeacher: boolean, isActive: boolean) => {
|
|
|
|
|
|
if (remoteUserId === user.id) return
|
|
|
|
|
|
if (!isActive) return
|
|
|
|
|
|
|
|
|
|
|
|
toast.push(<Notification title={`${name} sınıfa katıldı`} type="success" />, {
|
|
|
|
|
|
placement: 'top-end',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// State’e ekle
|
|
|
|
|
|
setParticipants((prev) => {
|
|
|
|
|
|
if (prev.find((p) => p.id === remoteUserId)) return prev
|
|
|
|
|
|
return [
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
{
|
|
|
|
|
|
id: remoteUserId,
|
|
|
|
|
|
name,
|
|
|
|
|
|
sessionId: classSession.id,
|
|
|
|
|
|
isTeacher,
|
|
|
|
|
|
isAudioMuted: classSettings.defaultMicrophoneState === 'muted',
|
|
|
|
|
|
isVideoMuted: classSettings.defaultCameraState === 'off',
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// PeerConnection hazırla
|
|
|
|
|
|
if (!webRTCServiceRef.current?.getPeerConnection(remoteUserId)) {
|
|
|
|
|
|
await webRTCServiceRef.current?.createPeerConnection(remoteUserId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔑 Çakışmayı önle: sadece id’si küçük olan offer başlatır
|
|
|
|
|
|
if (user.id < remoteUserId) {
|
|
|
|
|
|
const offer = await webRTCServiceRef.current!.createOffer(remoteUserId)
|
|
|
|
|
|
await signalRServiceRef.current?.sendOffer(classSession.id, remoteUserId, offer)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔑 Odaya girdiğinde var olan katılımcılar
|
|
|
|
|
|
signalRServiceRef.current.setExistingParticipantsHandler(
|
|
|
|
|
|
async (
|
|
|
|
|
|
existing: { userId: string; userName: string; isTeacher: boolean; isActive: boolean }[],
|
|
|
|
|
|
) => {
|
|
|
|
|
|
for (const participant of existing) {
|
|
|
|
|
|
if (!participant.isActive) continue
|
|
|
|
|
|
if (participant.userId === user.id) continue
|
|
|
|
|
|
|
|
|
|
|
|
// State’e ekle
|
|
|
|
|
|
setParticipants((prev) => {
|
|
|
|
|
|
if (prev.find((p) => p.id === participant.userId)) return prev
|
|
|
|
|
|
return [
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
{
|
|
|
|
|
|
id: participant.userId,
|
|
|
|
|
|
name: participant.userName,
|
|
|
|
|
|
sessionId: classSession.id,
|
|
|
|
|
|
isTeacher: participant.isTeacher,
|
|
|
|
|
|
isAudioMuted: classSettings.defaultMicrophoneState === 'muted',
|
|
|
|
|
|
isVideoMuted: classSettings.defaultCameraState === 'off',
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// PeerConnection hazırla
|
|
|
|
|
|
if (!webRTCServiceRef.current?.getPeerConnection(participant.userId)) {
|
|
|
|
|
|
await webRTCServiceRef.current?.createPeerConnection(participant.userId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔑 Çakışmayı önle
|
|
|
|
|
|
if (user.id < participant.userId) {
|
|
|
|
|
|
const offer = await webRTCServiceRef.current!.createOffer(participant.userId)
|
|
|
|
|
|
await signalRServiceRef.current?.sendOffer(classSession.id, participant.userId, offer)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
signalRServiceRef.current.setForceCleanupHandler(() => {
|
|
|
|
|
|
webRTCServiceRef.current?.closeAllConnections()
|
|
|
|
|
|
localStream?.getTracks().forEach((t) => t.stop())
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
signalRServiceRef.current.setParticipantLeaveHandler(({ userId, sessionId, userName }) => {
|
|
|
|
|
|
if (userId !== user.id) {
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification
|
|
|
|
|
|
title={`Katılımcı ayrıldı: ${userName ?? 'Bilinmeyen'}`}
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
/>,
|
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// peer connection’ı kapat
|
|
|
|
|
|
webRTCServiceRef.current?.closePeerConnection(userId)
|
|
|
|
|
|
|
|
|
|
|
|
// katılımcıyı state’den tamamen sil
|
|
|
|
|
|
setParticipants((prev) =>
|
|
|
|
|
|
prev.filter((p) => !(p.id === userId && p.sessionId === sessionId)),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (participants.find((p) => p.id === userId)?.isTeacher) {
|
|
|
|
|
|
setTeacherDisconnected(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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(async (userId, isMuted) => {
|
|
|
|
|
|
setParticipants((prev) =>
|
|
|
|
|
|
prev.map((p) => (p.id === userId ? { ...p, isAudioMuted: isMuted } : p)),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Eğer mute edilen kişi currentUser ise → kendi mikrofonunu kapat
|
|
|
|
|
|
if (userId === user.id) {
|
|
|
|
|
|
await webRTCServiceRef.current?.toggleAudio(!isMuted)
|
|
|
|
|
|
setIsAudioEnabled(!isMuted)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 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',
|
|
|
|
|
|
true,
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error) {
|
2026-07-06 09:44:26 +00:00
|
|
|
|
console.error('Sınıf servisleri başlatılamadı:', error)
|
2026-05-08 05:34:29 +00:00
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification
|
|
|
|
|
|
title="❌ Sınıf servisleri başlatılamadı. Bağlantınızı veya tarayıcı izinlerini kontrol edin."
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
/>,
|
|
|
|
|
|
{ placement: 'top-end' },
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cleanup = async () => {
|
|
|
|
|
|
if (signalRServiceRef.current) {
|
|
|
|
|
|
// ✅ önce LeaveClass
|
|
|
|
|
|
await signalRServiceRef.current.leaveClass(classSession.id)
|
|
|
|
|
|
// sonra disconnect
|
|
|
|
|
|
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 endVideoroom(classSession.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Bağlantıları kapat
|
|
|
|
|
|
await cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
// Başka sayfaya yönlendir
|
|
|
|
|
|
navigate(ROUTES_ENUM.protected.admin.videoroom.roomList)
|
|
|
|
|
|
} catch (err) {
|
2026-07-06 09:44:26 +00:00
|
|
|
|
console.error('Çıkış sırasında hata oluştu:', err)
|
2026-05-08 05:34:29 +00:00
|
|
|
|
toast.push(<Notification title="⚠️ Çıkış sırasında hata oluştu" type="warning" />, {
|
|
|
|
|
|
placement: 'top-end',
|
|
|
|
|
|
})
|
|
|
|
|
|
navigate(ROUTES_ENUM.protected.admin.videoroom.roomList)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 09:44:26 +00:00
|
|
|
|
const handleSendMessage = async (e: FormEvent) => {
|
2026-05-08 05:34:29 +00:00
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
if (newMessage.trim() && signalRServiceRef.current) {
|
|
|
|
|
|
if (messageMode === 'private' && selectedRecipient) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await signalRServiceRef.current.sendPrivateMessage(
|
|
|
|
|
|
classSession.id,
|
|
|
|
|
|
user.id,
|
|
|
|
|
|
user.name,
|
|
|
|
|
|
newMessage.trim(),
|
|
|
|
|
|
selectedRecipient.id,
|
|
|
|
|
|
selectedRecipient.name,
|
|
|
|
|
|
user.role === 'teacher',
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error) {
|
2026-07-06 09:44:26 +00:00
|
|
|
|
console.error('Özel mesaj gönderilemedi:', error)
|
2026-05-08 05:34:29 +00:00
|
|
|
|
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />, {
|
|
|
|
|
|
placement: 'top-end',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (messageMode === 'announcement' && user.role === 'teacher') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await signalRServiceRef.current.sendAnnouncement(
|
|
|
|
|
|
classSession.id,
|
|
|
|
|
|
user.id,
|
|
|
|
|
|
user.name,
|
|
|
|
|
|
newMessage.trim(),
|
|
|
|
|
|
user.role === 'teacher',
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error) {
|
2026-07-06 09:44:26 +00:00
|
|
|
|
console.error('Duyuru gönderilemedi:', error)
|
2026-05-08 05:34:29 +00:00
|
|
|
|
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />, {
|
|
|
|
|
|
placement: 'top-end',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await signalRServiceRef.current.sendChatMessage(
|
|
|
|
|
|
classSession.id,
|
|
|
|
|
|
user.id,
|
|
|
|
|
|
user.name,
|
|
|
|
|
|
newMessage.trim(),
|
|
|
|
|
|
user.role === 'teacher',
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error) {
|
2026-07-06 09:44:26 +00:00
|
|
|
|
console.error('Genel mesaj gönderilemedi:', error)
|
2026-05-08 05:34:29 +00:00
|
|
|
|
toast.push(<Notification title="❌ Genel mesaj gönderilemedi" type="danger" />, {
|
|
|
|
|
|
placement: 'top-end',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
setNewMessage('')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleMuteParticipant = async (
|
|
|
|
|
|
participantId: string,
|
|
|
|
|
|
isMuted: boolean,
|
|
|
|
|
|
isTeacher: boolean,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
if (signalRServiceRef.current && user.role === 'teacher') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await signalRServiceRef.current.muteParticipant(
|
|
|
|
|
|
classSession.id,
|
|
|
|
|
|
participantId,
|
|
|
|
|
|
isMuted,
|
|
|
|
|
|
isTeacher,
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (err) {
|
2026-07-06 09:44:26 +00:00
|
|
|
|
console.error('Katılımcı susturulamadı:', err)
|
2026-05-08 05:34:29 +00:00
|
|
|
|
toast.push(<Notification title="❌ Katılımcı susturulamadı" type="danger" />, {
|
|
|
|
|
|
placement: 'top-end',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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, participantName: string) => {
|
|
|
|
|
|
if (signalRServiceRef.current && user.role === 'teacher') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await signalRServiceRef.current.kickParticipant(
|
|
|
|
|
|
classSession.id,
|
|
|
|
|
|
participantId,
|
|
|
|
|
|
participantName,
|
|
|
|
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error) {
|
2026-07-06 09:44:26 +00:00
|
|
|
|
console.error('Katılımcı atılamadı:', error)
|
2026-05-08 05:34:29 +00:00
|
|
|
|
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />, {
|
|
|
|
|
|
placement: 'top-end',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleUploadDocument = async (file: File) => {
|
|
|
|
|
|
// In a real app, this would upload to a server
|
|
|
|
|
|
const newDoc: VideoroomDocumentDto = {
|
|
|
|
|
|
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: VideoroomDocumentDto) => {
|
|
|
|
|
|
window.open(document.url, '_blank')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleStartScreenShare = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. sadece ekran videosu al
|
|
|
|
|
|
const screen = await navigator.mediaDevices.getDisplayMedia({
|
|
|
|
|
|
video: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 2. mikrofonu ayrı al
|
|
|
|
|
|
let mic: MediaStream | null = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
mic = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
|
|
|
} catch (err) {
|
2026-07-06 09:44:26 +00:00
|
|
|
|
console.error('Mikrofon alınamadı:', err)
|
2026-05-08 05:34:29 +00:00
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification
|
|
|
|
|
|
title="⚠️ Mikrofon alınamadı. Sadece ekran paylaşımı yapılacak."
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
/>,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. merge et
|
|
|
|
|
|
if (mic) {
|
|
|
|
|
|
mic.getAudioTracks().forEach((track) => screen.addTrack(track))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setScreenStream(screen)
|
|
|
|
|
|
setIsScreenSharing(true)
|
|
|
|
|
|
setScreenSharer(user.name)
|
|
|
|
|
|
webRTCServiceRef.current?.addStreamToPeers(screen)
|
|
|
|
|
|
|
|
|
|
|
|
// Handle stream end
|
|
|
|
|
|
screen.getVideoTracks()[0].onended = () => {
|
|
|
|
|
|
handleStopScreenShare()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-07-06 09:44:26 +00:00
|
|
|
|
console.error('Ekran paylaşımı başlatılamadı:', error)
|
2026-05-08 05:34:29 +00:00
|
|
|
|
toast.push(<Notification title="❌ Ekran paylaşımı başlatılamadı" type="danger" />, {
|
|
|
|
|
|
placement: 'top-end',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleStopScreenShare = () => {
|
|
|
|
|
|
if (screenStream) {
|
|
|
|
|
|
// PeerConnections’tan kaldır
|
|
|
|
|
|
screenStream.getTracks().forEach((track) => {
|
|
|
|
|
|
webRTCServiceRef.current?.removeTrackFromPeers(track)
|
|
|
|
|
|
track.stop()
|
|
|
|
|
|
})
|
|
|
|
|
|
setScreenStream(undefined)
|
|
|
|
|
|
}
|
|
|
|
|
|
setIsScreenSharing(false)
|
|
|
|
|
|
setScreenSharer(undefined)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleLayoutChange = (layout: VideoroomLayoutDto) => {
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 getFileIcon = (type: string) => {
|
|
|
|
|
|
if (type.includes('pdf')) return <FaFilePdf className="text-red-500" />
|
|
|
|
|
|
if (
|
|
|
|
|
|
type.includes('word') ||
|
|
|
|
|
|
type.includes('doc') ||
|
|
|
|
|
|
type.includes('presentation') ||
|
|
|
|
|
|
type.includes('powerpoint')
|
|
|
|
|
|
)
|
|
|
|
|
|
return <FaFileWord className="text-blue-500" />
|
|
|
|
|
|
if (type.includes('image')) return <FaFileImage className="text-green-500" />
|
|
|
|
|
|
return <FaFileAlt className="text-gray-500" />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ...existing code...
|
|
|
|
|
|
// ...existing code...
|
|
|
|
|
|
// --- yan panel fonksiyonu ---
|
|
|
|
|
|
const renderSidePanel = () => {
|
|
|
|
|
|
if (!activeSidePanel) return null
|
|
|
|
|
|
|
|
|
|
|
|
switch (activeSidePanel) {
|
|
|
|
|
|
case 'chat':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ChatPanel
|
|
|
|
|
|
user={user}
|
|
|
|
|
|
participants={participants}
|
|
|
|
|
|
chatMessages={chatMessages}
|
|
|
|
|
|
newMessage={newMessage}
|
|
|
|
|
|
setNewMessage={setNewMessage}
|
|
|
|
|
|
messageMode={messageMode}
|
|
|
|
|
|
setMessageMode={setMessageMode}
|
|
|
|
|
|
selectedRecipient={selectedRecipient}
|
|
|
|
|
|
setSelectedRecipient={setSelectedRecipient}
|
|
|
|
|
|
onSendMessage={handleSendMessage}
|
|
|
|
|
|
onClose={() => setActiveSidePanel(null)}
|
|
|
|
|
|
formatTime={formatTime}
|
|
|
|
|
|
classSettings={classSettings}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
case 'participants':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ParticipantsPanel
|
|
|
|
|
|
user={user}
|
|
|
|
|
|
participants={participants}
|
|
|
|
|
|
attendanceRecords={attendanceRecords}
|
|
|
|
|
|
onMuteParticipant={handleMuteParticipant}
|
|
|
|
|
|
onKickParticipant={handleKickParticipant}
|
|
|
|
|
|
onDismissHandRaise={handleDismissHandRaise}
|
|
|
|
|
|
onClose={() => setActiveSidePanel(null)}
|
|
|
|
|
|
formatTime={formatTime}
|
|
|
|
|
|
formatDuration={formatDuration}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ...existing code...
|
|
|
|
|
|
case 'documents':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<DocumentsPanel
|
|
|
|
|
|
user={user}
|
|
|
|
|
|
documents={documents}
|
|
|
|
|
|
onUpload={handleUploadDocument}
|
|
|
|
|
|
onDelete={handleDeleteDocument}
|
|
|
|
|
|
onView={handleViewDocument}
|
|
|
|
|
|
onClose={() => setActiveSidePanel(null)}
|
|
|
|
|
|
formatFileSize={formatFileSize}
|
|
|
|
|
|
getFileIcon={getFileIcon}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
case 'layout':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<LayoutPanel
|
|
|
|
|
|
layouts={layouts}
|
|
|
|
|
|
currentLayout={currentLayout ?? layouts[0]}
|
|
|
|
|
|
onChangeLayout={handleLayoutChange}
|
|
|
|
|
|
onClose={() => setActiveSidePanel(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
window.addEventListener('beforeunload', handleLeaveCall)
|
|
|
|
|
|
window.addEventListener('pagehide', handleLeaveCall)
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener('beforeunload', handleLeaveCall)
|
|
|
|
|
|
window.removeEventListener('pagehide', handleLeaveCall)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [classSession.id])
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Helmet
|
|
|
|
|
|
titleTemplate="%s | Erp Platform"
|
|
|
|
|
|
title={translate('::' + 'App.Videoroom.RoomDetail')}
|
|
|
|
|
|
defaultTitle="Erp Platform"
|
|
|
|
|
|
></Helmet>
|
|
|
|
|
|
|
|
|
|
|
|
{teacherDisconnected && (
|
|
|
|
|
|
<div className="bg-red-600 text-white p-2 text-center">
|
|
|
|
|
|
Öğretmenin bağlantısı koptu, tekrar bağlanmasını bekleyin...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="min-h-screen h-screen flex flex-col bg-gray-900 text-white overflow-hidden">
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
|
exit={{ opacity: 0 }}
|
|
|
|
|
|
className="h-full flex flex-col relative overflow-hidden"
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Main Content Area */}
|
|
|
|
|
|
<div className="flex-1 flex flex-col lg:flex-row relative min-h-0 h-full">
|
|
|
|
|
|
{/* Left Content Area - Video and Screen Share */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`flex-1 flex flex-col min-h-0 h-full transition-all duration-300 ${!activeSidePanel ? 'flex items-center justify-center' : ''}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Video Container - Panel kapalıyken ortalanmış */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`${!activeSidePanel ? 'w-full max-w-6xl' : 'w-full h-full'} flex flex-col min-h-0 h-full`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Screen Share Panel */}
|
|
|
|
|
|
{(isScreenSharing || screenStream) && (
|
|
|
|
|
|
<div className="p-2 sm:p-4 flex-shrink-0">
|
|
|
|
|
|
<ScreenSharePanel
|
|
|
|
|
|
isSharing={isScreenSharing}
|
|
|
|
|
|
onStartShare={handleStartScreenShare}
|
|
|
|
|
|
onStopShare={handleStopScreenShare}
|
|
|
|
|
|
sharedScreen={screenStream}
|
|
|
|
|
|
sharerName={screenSharer}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* Video Grid */}
|
|
|
|
|
|
<div className="flex-1 relative overflow-hidden min-h-0 h-full">
|
|
|
|
|
|
<RoomParticipant
|
|
|
|
|
|
participants={participants.filter((p) => p.isActive)}
|
|
|
|
|
|
localStream={user.role === 'observer' ? undefined : localStream}
|
|
|
|
|
|
currentUserId={user.id}
|
|
|
|
|
|
currentUserName={user.name}
|
|
|
|
|
|
isTeacher={user.role === 'teacher'}
|
|
|
|
|
|
isAudioEnabled={isAudioEnabled}
|
|
|
|
|
|
isVideoEnabled={isVideoEnabled}
|
|
|
|
|
|
onMuteParticipant={handleMuteParticipant}
|
|
|
|
|
|
layout={currentLayout ?? layouts[0]}
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Side Panel */}
|
|
|
|
|
|
{activeSidePanel && (
|
|
|
|
|
|
<div className="fixed inset-0 z-40 bg-white flex flex-col w-full h-full lg:relative lg:inset-auto lg:w-80 lg:h-full lg:z-0 lg:bg-white">
|
|
|
|
|
|
{renderSidePanel()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Bottom Control Bar - Google Meet Style */}
|
|
|
|
|
|
<div className="bg-gray-800 p-2 sm:p-3 flex-shrink-0">
|
|
|
|
|
|
{/* Mobile Layout */}
|
|
|
|
|
|
<div className="flex lg:hidden items-center justify-between">
|
|
|
|
|
|
{/* Left Side - Main Controls */}
|
|
|
|
|
|
<div className="flex items-center space-x-1 sm:space-x-2">
|
|
|
|
|
|
{/* Audio Control */}
|
|
|
|
|
|
{user.role !== 'observer' && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={handleToggleAudio}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-2 text-white transition-all sm:!p-3 ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
isAudioEnabled
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-gray-700 hover:!bg-gray-600'
|
|
|
|
|
|
: '!bg-red-600 hover:!bg-red-700'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Video Control */}
|
|
|
|
|
|
{user.role !== 'observer' && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={handleToggleVideo}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-2 text-white transition-all sm:!p-3 ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
isVideoEnabled
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-gray-700 hover:!bg-gray-600'
|
|
|
|
|
|
: '!bg-red-600 hover:!bg-red-700'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Screen Share */}
|
|
|
|
|
|
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={isScreenSharing ? handleStopScreenShare : handleStartScreenShare}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-2 text-white transition-all sm:!p-3 ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
isScreenSharing
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-blue-600 hover:!bg-blue-700'
|
|
|
|
|
|
: '!bg-gray-700 hover:!bg-gray-600'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaDesktop size={16} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Hand Raise (Students) */}
|
|
|
|
|
|
{user.role === 'student' && classSettings.allowHandRaise && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={handleRaiseHand}
|
|
|
|
|
|
disabled={hasRaisedHand}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-2 text-white transition-all sm:!p-3 ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
hasRaisedHand
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-yellow-600 cursor-not-allowed'
|
|
|
|
|
|
: '!bg-gray-700 hover:!bg-yellow-600'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaHandPaper size={16} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Leave Call */}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={handleLeaveCall}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-red-600 !p-2 text-white transition-all hover:!bg-red-700 sm:!p-3"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
title="Aramayı Sonlandır"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaPhone size={16} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Right Side - Panel Controls */}
|
|
|
|
|
|
<div className="flex items-center">
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
|
|
|
|
|
className="!inline-flex !h-auto items-center justify-center rounded-lg !bg-gray-700 !p-2 text-white hover:!bg-gray-600"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => setMobileMenuOpen(true)}
|
|
|
|
|
|
aria-label="Menüyü Aç"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaBars size={20} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Hamburger Menu Modal */}
|
|
|
|
|
|
{mobileMenuOpen && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Overlay */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="fixed inset-0 z-40 bg-black bg-opacity-40"
|
|
|
|
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* Drawer */}
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ x: '100%' }}
|
|
|
|
|
|
animate={{ x: 0 }}
|
|
|
|
|
|
exit={{ x: '100%' }}
|
|
|
|
|
|
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
|
|
|
|
|
className="fixed inset-0 z-50 w-full h-full bg-white shadow-2xl flex flex-col p-0 lg:top-0 lg:right-0 lg:w-80 lg:h-full lg:inset-y-0 lg:left-auto"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center justify-between px-4 py-4 border-b">
|
|
|
|
|
|
<span className="font-semibold text-gray-800 text-lg">Menü</span>
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => setMobileMenuOpen(false)}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-transparent !p-2 hover:!bg-gray-100"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
>
|
|
|
|
|
|
<FaTimes size={22} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 flex flex-col space-y-1 px-2 py-2 overflow-y-auto">
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setMobileMenuOpen(false)
|
|
|
|
|
|
setTimeout(() => toggleSidePanel('chat'), 200)
|
|
|
|
|
|
}}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center gap-2 rounded-lg !p-3 text-base transition-all ${activeSidePanel === 'chat' ? '!bg-blue-100 text-blue-700' : '!bg-transparent text-gray-700 hover:!bg-gray-100'}`}
|
2026-05-08 05:34:29 +00:00
|
|
|
|
>
|
|
|
|
|
|
<FaComments /> <span>Sohbet</span>
|
|
|
|
|
|
{chatMessages.length > 0 && (
|
|
|
|
|
|
<span className="ml-auto bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
|
|
|
|
|
|
{chatMessages.length > 9 ? '9+' : chatMessages.length}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setMobileMenuOpen(false)
|
|
|
|
|
|
setTimeout(() => toggleSidePanel('participants'), 200)
|
|
|
|
|
|
}}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center gap-2 rounded-lg !p-3 text-base transition-all relative ${activeSidePanel === 'participants' ? '!bg-blue-100 text-blue-700' : '!bg-transparent text-gray-700 hover:!bg-gray-100'}`}
|
2026-05-08 05:34:29 +00:00
|
|
|
|
>
|
|
|
|
|
|
<FaUserFriends />
|
|
|
|
|
|
<span>Katılımcılar</span>
|
|
|
|
|
|
{/* El kaldıran badge */}
|
|
|
|
|
|
{raisedHandsCount > 0 && (
|
|
|
|
|
|
<span className="absolute top-2 right-3 bg-yellow-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
|
|
|
|
|
|
{raisedHandsCount > 9 ? '9+' : raisedHandsCount}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setMobileMenuOpen(false)
|
|
|
|
|
|
setTimeout(() => toggleSidePanel('documents'), 200)
|
|
|
|
|
|
}}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center gap-2 rounded-lg !p-3 text-base transition-all ${activeSidePanel === 'documents' ? '!bg-blue-100 text-blue-700' : '!bg-transparent text-gray-700 hover:!bg-gray-100'}`}
|
2026-05-08 05:34:29 +00:00
|
|
|
|
>
|
|
|
|
|
|
<FaFile /> <span>Dokümanlar</span>
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setMobileMenuOpen(false)
|
|
|
|
|
|
setTimeout(() => toggleSidePanel('layout'), 200)
|
|
|
|
|
|
}}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center gap-2 rounded-lg !p-3 text-base transition-all ${activeSidePanel === 'layout' ? '!bg-blue-100 text-blue-700' : '!bg-transparent text-gray-700 hover:!bg-gray-100'}`}
|
2026-05-08 05:34:29 +00:00
|
|
|
|
>
|
|
|
|
|
|
<FaLayerGroup /> <span>Görünüm</span>
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
{user.role === 'teacher' && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setMobileMenuOpen(false)
|
|
|
|
|
|
setTimeout(() => handleMuteAll(), 200)
|
|
|
|
|
|
}}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className="!inline-flex !h-auto items-center gap-2 rounded-lg !bg-transparent !p-3 text-base text-gray-700 transition-all hover:!bg-gray-100"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
>
|
|
|
|
|
|
{isAllMuted ? <FaVolumeUp /> : <FaVolumeMute />}{' '}
|
|
|
|
|
|
<span>{isAllMuted ? 'Hepsinin Sesini Aç' : 'Hepsini Sustur'}</span>
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setMobileMenuOpen(false)
|
|
|
|
|
|
setTimeout(() => toggleFullscreen(), 200)
|
|
|
|
|
|
}}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className="!inline-flex !h-auto items-center gap-2 rounded-lg !bg-transparent !p-3 text-base text-gray-700 transition-all hover:!bg-gray-100"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
>
|
|
|
|
|
|
{isFullscreen ? <FaCompress /> : <FaExpand />}{' '}
|
|
|
|
|
|
<span>{isFullscreen ? 'Tam Ekrandan Çık' : 'Tam Ekran'}</span>
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Desktop Layout */}
|
|
|
|
|
|
<div className="hidden lg:flex items-center justify-center relative">
|
|
|
|
|
|
{/* Left Side - Meeting Info */}
|
|
|
|
|
|
<div className="flex items-center space-x-4 text-white absolute left-0">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<span className="text-sm font-medium truncate">{classSession?.name}</span>
|
|
|
|
|
|
<div className="w-px h-4 bg-gray-600"></div>
|
|
|
|
|
|
<span className="text-sm text-gray-300">
|
|
|
|
|
|
{new Date().toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' })}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Center - Main Controls */}
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
{/* Audio Control */}
|
|
|
|
|
|
{user.role !== 'observer' && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={handleToggleAudio}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-3 text-white transition-all ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
isAudioEnabled
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-gray-700 hover:!bg-gray-600'
|
|
|
|
|
|
: '!bg-red-600 hover:!bg-red-700'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Video Control */}
|
|
|
|
|
|
{user.role !== 'observer' && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={handleToggleVideo}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-3 text-white transition-all ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
isVideoEnabled
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-gray-700 hover:!bg-gray-600'
|
|
|
|
|
|
: '!bg-red-600 hover:!bg-red-700'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Screen Share */}
|
|
|
|
|
|
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={isScreenSharing ? handleStopScreenShare : handleStartScreenShare}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-3 text-white transition-all ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
isScreenSharing
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-blue-600 hover:!bg-blue-700'
|
|
|
|
|
|
: '!bg-gray-700 hover:!bg-gray-600'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaDesktop size={16} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Hand Raise (Students) */}
|
|
|
|
|
|
{user.role === 'student' && classSettings.allowHandRaise && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={handleRaiseHand}
|
|
|
|
|
|
disabled={hasRaisedHand}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-3 text-white transition-all ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
hasRaisedHand
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-yellow-600 cursor-not-allowed'
|
|
|
|
|
|
: '!bg-gray-700 hover:!bg-yellow-600'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaHandPaper size={16} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Leave Call */}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="circle"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={handleLeaveCall}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-red-600 !p-3 text-white transition-all hover:!bg-red-700"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
title="Aramayı Sonlandır"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaPhone size={16} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Right Side - Panel Controls */}
|
|
|
|
|
|
<div className="flex items-center space-x-2 absolute right-0">
|
|
|
|
|
|
{/* Fullscreen Toggle */}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={toggleFullscreen}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className="!inline-flex !h-auto items-center justify-center rounded-lg !bg-gray-700 !p-2 text-white transition-all hover:!bg-gray-600"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
title={isFullscreen ? 'Tam Ekrandan Çık' : 'Tam Ekran'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isFullscreen ? <FaCompress size={14} /> : <FaExpand size={14} />}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
|
|
|
|
|
|
{/* Chat */}
|
|
|
|
|
|
{((user.role !== 'observer' && classSettings.allowStudentChat) ||
|
|
|
|
|
|
user.role === 'teacher') && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => toggleSidePanel('chat')}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-lg !p-2 transition-all relative ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
activeSidePanel === 'chat'
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-blue-600 text-white'
|
|
|
|
|
|
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title="Sohbet"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaComments size={14} />
|
|
|
|
|
|
{chatMessages.length > 0 && (
|
|
|
|
|
|
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
|
|
|
|
|
|
{chatMessages.length > 9 ? '9+' : chatMessages.length}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Participants */}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => toggleSidePanel('participants')}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-lg !p-2 transition-all relative ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
activeSidePanel === 'participants'
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-blue-600 text-white'
|
|
|
|
|
|
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title="Katılımcılar"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaUserFriends size={14} />
|
|
|
|
|
|
{/* El kaldıran badge */}
|
|
|
|
|
|
{raisedHandsCount > 0 && (
|
|
|
|
|
|
<span className="absolute -top-1 -right-3 bg-yellow-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
|
|
|
|
|
|
{raisedHandsCount > 9 ? '9+' : raisedHandsCount}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
|
|
|
|
|
|
{/* Teacher Only Options */}
|
|
|
|
|
|
{user.role === 'teacher' && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Documents Button */}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => toggleSidePanel('documents')}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-lg !p-2 transition-all ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
activeSidePanel === 'documents'
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-blue-600 text-white'
|
|
|
|
|
|
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title="Dokümanlar"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaFile size={14} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
|
|
|
|
|
|
{/* Mute All Button */}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={handleMuteAll}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className="!inline-flex !h-auto items-center justify-center rounded-lg !bg-gray-700 !p-2 text-white transition-all hover:!bg-gray-600"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
title={isAllMuted ? 'Hepsinin Sesini Aç' : 'Hepsini Sustur'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isAllMuted ? <FaVolumeUp size={14} /> : <FaVolumeMute size={14} />}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Layout Button */}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-05-08 05:34:29 +00:00
|
|
|
|
onClick={() => toggleSidePanel('layout')}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
className={`!inline-flex !h-auto items-center justify-center rounded-lg !p-2 transition-all ${
|
2026-05-08 05:34:29 +00:00
|
|
|
|
activeSidePanel === 'layout'
|
2026-06-25 22:55:44 +00:00
|
|
|
|
? '!bg-blue-600 text-white'
|
|
|
|
|
|
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
2026-05-08 05:34:29 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title="Layout"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaLayerGroup size={14} />
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-08 05:34:29 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Kick Participant Modal */}
|
|
|
|
|
|
<KickParticipantModal
|
|
|
|
|
|
participant={kickingParticipant}
|
|
|
|
|
|
isOpen={!!kickingParticipant}
|
|
|
|
|
|
onClose={() => setKickingParticipant(null)}
|
|
|
|
|
|
onConfirm={handleKickParticipant}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default RoomDetail
|