erp-platform/ui/src/views/classroom/RoomDetail.tsx
2025-08-30 01:53:59 +03:00

1358 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ClassroomDto>(newClassSession)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [participants, setParticipants] = useState<ClassroomParticipantDto[]>([])
const [localStream, setLocalStream] = useState<MediaStream>()
const [isAudioEnabled, setIsAudioEnabled] = useState(true)
const [isVideoEnabled, setIsVideoEnabled] = useState(true)
const [attendanceRecords, setAttendanceRecords] = useState<ClassroomAttendanceDto[]>([])
const [chatMessages, setChatMessages] = useState<ClassroomChatDto[]>([])
const [currentLayout, setCurrentLayout] = useState<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',
})
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<ClassDocumentDto[]>([])
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<'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<HTMLInputElement>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
const [classSettings, setClassSettings] = useState<ClassroomSettingsDto>({
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 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) {
// idsi 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ı stateden 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 stateini 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<ClassroomSettingsDto>) => {
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 <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]
}
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<HTMLInputElement>) => {
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 <FaTh size={24} />
case 'speaker':
return <FaExpand size={24} />
case 'presentation':
return <FaDesktop size={24} />
case 'sidebar':
return <FaUsers size={24} />
default:
return <FaTh size={24} />
}
}
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}
/>
)
case 'participants':
return (
<ParticipantsPanel
user={user}
participants={participants}
attendanceRecords={attendanceRecords}
onMuteParticipant={handleMuteParticipant}
onKickParticipant={handleKickParticipant}
onClose={() => setActiveSidePanel(null)}
formatTime={formatTime}
formatDuration={formatDuration}
/>
)
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}
onChangeLayout={handleLayoutChange}
onClose={() => setActiveSidePanel(null)}
/>
)
case 'settings':
return (
<SettingsPanel
user={user}
classSettings={classSettings}
onSettingsChange={handleSettingsChange}
onClose={() => setActiveSidePanel(null)}
/>
)
default:
return null
}
}
return (
<>
<Helmet
titleTemplate="%s | Kurs Platform"
title={translate('::' + 'App.Classroom.RoomDetail')}
defaultTitle="Kurs Platform"
></Helmet>
<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">
<ParticipantGrid
participants={participants}
localStream={user.role === 'observer' ? undefined : localStream}
currentUserId={user.id}
currentUserName={user.name}
isTeacher={user.role === 'teacher'}
isAudioEnabled={isAudioEnabled}
isVideoEnabled={isVideoEnabled}
onToggleAudio={user.role === 'observer' ? () => {} : 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
}
/>
</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' && (
<button
onClick={handleToggleAudio}
className={`p-2 sm:p-3 rounded-full transition-all ${
isAudioEnabled
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-red-600 hover:bg-red-700 text-white'
}`}
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
>
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
</button>
)}
{/* Video Control */}
{user.role !== 'observer' && (
<button
onClick={handleToggleVideo}
className={`p-2 sm:p-3 rounded-full transition-all ${
isVideoEnabled
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-red-600 hover:bg-red-700 text-white'
}`}
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
>
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
</button>
)}
{/* Screen Share */}
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
<button
onClick={isScreenSharing ? handleStopScreenShare : handleStartScreenShare}
className={`p-2 sm:p-3 rounded-full transition-all ${
isScreenSharing
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
>
<FaDesktop size={16} />
</button>
)}
{/* Hand Raise (Students) */}
{user.role === 'student' && classSettings.allowHandRaise && (
<button
onClick={handleRaiseHand}
disabled={hasRaisedHand}
className={`p-2 sm:p-3 rounded-full transition-all ${
hasRaisedHand
? 'bg-yellow-600 text-white cursor-not-allowed'
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
}`}
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
>
<FaHandPaper size={16} />
</button>
)}
{/* Leave Call */}
<button
onClick={handleLeaveCall}
className="p-2 sm:p-3 rounded-full bg-red-600 hover:bg-red-700 text-white transition-all"
title="Aramayı Sonlandır"
>
<FaPhone size={16} />
</button>
</div>
{/* Right Side - Panel Controls */}
<div className="flex items-center">
<button
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-white"
onClick={() => setMobileMenuOpen(true)}
aria-label="Menüyü Aç"
>
<FaBars size={20} />
</button>
</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>
<button
onClick={() => setMobileMenuOpen(false)}
className="p-2 rounded-full hover:bg-gray-100"
>
<FaTimes size={22} />
</button>
</div>
<div className="flex-1 flex flex-col space-y-1 px-2 py-2 overflow-y-auto">
<button
onClick={() => {
setMobileMenuOpen(false)
setTimeout(() => toggleSidePanel('chat'), 200)
}}
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base ${activeSidePanel === 'chat' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
>
<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>
)}
</button>
<button
onClick={() => {
setMobileMenuOpen(false)
setTimeout(() => toggleSidePanel('participants'), 200)
}}
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base ${activeSidePanel === 'participants' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
>
<FaUserFriends /> <span>Katılımcılar</span>
</button>
<button
onClick={() => {
setMobileMenuOpen(false)
setTimeout(() => toggleSidePanel('documents'), 200)
}}
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base ${activeSidePanel === 'documents' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
>
<FaFile /> <span>Dokümanlar</span>
</button>
<button
onClick={() => {
setMobileMenuOpen(false)
setTimeout(() => toggleSidePanel('layout'), 200)
}}
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base ${activeSidePanel === 'layout' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
>
<FaLayerGroup /> <span>Görünüm</span>
</button>
<button
onClick={() => {
setMobileMenuOpen(false)
setTimeout(() => toggleSidePanel('settings'), 200)
}}
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base ${activeSidePanel === 'settings' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
>
<FaWrench /> <span>Ayarlar</span>
</button>
{user.role === 'teacher' && (
<>
<button
onClick={() => {
setMobileMenuOpen(false)
setTimeout(() => simulateStudentJoin(), 200)
}}
className="flex items-center space-x-2 p-3 rounded-lg transition-all hover:bg-gray-100 text-gray-700 text-base"
>
<FaUserPlus /> <span>Öğrenci Ekle (Demo)</span>
</button>
<button
onClick={() => {
setMobileMenuOpen(false)
setTimeout(() => handleMuteAll(), 200)
}}
className="flex items-center space-x-2 p-3 rounded-lg transition-all hover:bg-gray-100 text-gray-700 text-base"
>
{isAllMuted ? <FaVolumeUp /> : <FaVolumeMute />}{' '}
<span>{isAllMuted ? 'Hepsinin Sesini Aç' : 'Hepsini Sustur'}</span>
</button>
</>
)}
<button
onClick={() => {
setMobileMenuOpen(false)
setTimeout(() => toggleFullscreen(), 200)
}}
className="flex items-center space-x-2 p-3 rounded-lg transition-all hover:bg-gray-100 text-gray-700 text-base"
>
{isFullscreen ? <FaCompress /> : <FaExpand />}{' '}
<span>{isFullscreen ? 'Tam Ekrandan Çık' : 'Tam Ekran'}</span>
</button>
</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' && (
<button
onClick={handleToggleAudio}
className={`p-3 rounded-full transition-all ${
isAudioEnabled
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-red-600 hover:bg-red-700 text-white'
}`}
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
>
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
</button>
)}
{/* Video Control */}
{user.role !== 'observer' && (
<button
onClick={handleToggleVideo}
className={`p-3 rounded-full transition-all ${
isVideoEnabled
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-red-600 hover:bg-red-700 text-white'
}`}
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
>
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
</button>
)}
{/* Screen Share */}
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
<button
onClick={isScreenSharing ? handleStopScreenShare : handleStartScreenShare}
className={`p-3 rounded-full transition-all ${
isScreenSharing
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
>
<FaDesktop size={16} />
</button>
)}
{/* Hand Raise (Students) */}
{user.role === 'student' && classSettings.allowHandRaise && (
<button
onClick={handleRaiseHand}
disabled={hasRaisedHand}
className={`p-3 rounded-full transition-all ${
hasRaisedHand
? 'bg-yellow-600 text-white cursor-not-allowed'
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
}`}
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
>
<FaHandPaper size={16} />
</button>
)}
{/* Leave Call */}
<button
onClick={handleLeaveCall}
className="p-3 rounded-full bg-red-600 hover:bg-red-700 text-white transition-all"
title="Aramayı Sonlandır"
>
<FaPhone size={16} />
</button>
</div>
{/* Right Side - Panel Controls & Participant Count */}
<div className="flex items-center space-x-2 absolute right-0">
{/* Participant Count */}
<div className="text-white text-sm mr-2">
<FaUsers size={14} className="inline mr-1" />
{participants.length + 1}
</div>
{/* Fullscreen Toggle */}
<button
onClick={toggleFullscreen}
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-white transition-all"
title={isFullscreen ? 'Tam Ekrandan Çık' : 'Tam Ekran'}
>
{isFullscreen ? <FaCompress size={14} /> : <FaExpand size={14} />}
</button>
{/* Chat */}
{((user.role !== 'observer' && classSettings.allowStudentChat) ||
user.role === 'teacher') && (
<button
onClick={() => toggleSidePanel('chat')}
className={`p-2 rounded-lg transition-all relative ${
activeSidePanel === 'chat'
? 'bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
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>
)}
</button>
)}
{/* Parmak Kaldır (Öğrenci) */}
{user.role === 'student' && classSettings.allowHandRaise && (
<button
onClick={handleRaiseHand}
disabled={hasRaisedHand}
className={`p-2 rounded-lg transition-all ${
hasRaisedHand
? 'bg-yellow-600 text-white cursor-not-allowed'
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
}`}
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
>
<FaHandPaper size={14} />
</button>
)}
{/* Participants */}
<button
onClick={() => toggleSidePanel('participants')}
className={`p-2 rounded-lg transition-all ${
activeSidePanel === 'participants'
? 'bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
title="Katılımcılar"
>
<FaUserFriends size={14} />
</button>
{/* Teacher Only Options */}
{user.role === 'teacher' && (
<>
{/* Documents Button */}
<button
onClick={() => toggleSidePanel('documents')}
className={`p-2 rounded-lg transition-all ${
activeSidePanel === 'documents'
? 'bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
title="Dokümanlar"
>
<FaFile size={14} />
</button>
{/* Mute All Button */}
<button
onClick={handleMuteAll}
className="p-2 rounded-lg transition-all bg-gray-700 hover:bg-gray-600 text-white"
title={isAllMuted ? 'Hepsinin Sesini Aç' : 'Hepsini Sustur'}
>
{isAllMuted ? <FaVolumeUp size={14} /> : <FaVolumeMute size={14} />}
</button>
{/* Add Student Demo Button */}
<button
onClick={simulateStudentJoin}
className="p-2 rounded-lg transition-all bg-gray-700 hover:bg-gray-600 text-white"
title="Öğrenci Ekle (Demo)"
>
<FaUserPlus size={14} />
</button>
</>
)}
{/* Layout Button */}
<button
onClick={() => toggleSidePanel('layout')}
className={`p-2 rounded-lg transition-all ${
activeSidePanel === 'layout'
? 'bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
title="Layout"
>
<FaLayerGroup size={14} />
</button>
{/* Settings Button */}
<button
onClick={() => toggleSidePanel('settings')}
className={`p-2 rounded-lg transition-all ${
activeSidePanel === 'settings'
? 'bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
title="Ayarlar"
>
<FaWrench size={14} />
</button>
</div>
</div>
</div>
{/* Kick Participant Modal */}
<KickParticipantModal
participant={kickingParticipant}
isOpen={!!kickingParticipant}
onClose={() => setKickingParticipant(null)}
onConfirm={handleKickParticipant}
/>
</motion.div>
</div>
</>
)
}
export default RoomDetail