erp-platform/ui/src/views/classroom/RoomDetail.tsx

1243 lines
45 KiB
TypeScript
Raw Normal View History

2025-08-26 08:39:09 +00:00
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 {
2025-08-29 09:37:38 +00:00
ClassroomAttendanceDto,
ClassroomChatDto,
2025-08-26 08:39:09 +00:00
ClassDocumentDto,
2025-08-29 09:37:38 +00:00
ClassroomParticipantDto,
2025-08-26 08:39:09 +00:00
ClassroomDto,
ClassroomSettingsDto,
VideoLayoutDto,
} from '@/proxy/classroom/models'
import { useStoreState } from '@/store/store'
2025-08-26 14:57:09 +00:00
import { KickParticipantModal } from '@/components/classroom/KickParticipantModal'
import { useParams } from 'react-router-dom'
import {
getClassroomAttandances,
getClassroomById,
getClassroomChats,
} from '@/services/classroom.service'
2025-08-27 15:00:22 +00:00
import { showDbDateAsIs } from '@/utils/dateUtils'
2025-08-28 11:53:47 +00:00
import { useNavigate } from 'react-router-dom'
import { endClassroom } from '@/services/classroom.service'
import { ROUTES_ENUM } from '@/routes/route.constant'
2025-08-29 09:37:38 +00:00
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'
2025-08-26 08:39:09 +00:00
type SidePanelType =
| 'chat'
| 'participants'
| 'documents'
| 'handraises'
| 'layout'
| 'settings'
| null
2025-08-26 14:57:09 +00:00
const newClassSession: ClassroomDto = {
id: '',
name: '',
teacherId: '',
teacherName: '',
scheduledStartTime: '',
2025-08-28 11:53:47 +00:00
scheduledEndTime: '',
actualStartTime: '',
2025-08-28 11:53:47 +00:00
actualEndTime: '',
2025-08-26 14:57:09 +00:00
participantCount: 0,
2025-08-27 20:55:01 +00:00
settingsDto: undefined,
2025-08-26 14:57:09 +00:00
}
const RoomDetail: React.FC = () => {
const params = useParams()
2025-08-28 11:53:47 +00:00
const navigate = useNavigate()
2025-08-26 08:39:09 +00:00
const { user } = useStoreState((state) => state.auth)
2025-08-29 09:37:38 +00:00
const { translate } = useLocalization()
2025-08-26 14:57:09 +00:00
const [classSession, setClassSession] = useState<ClassroomDto>(newClassSession)
2025-08-26 08:39:09 +00:00
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
2025-08-29 09:37:38 +00:00
const [participants, setParticipants] = useState<ClassroomParticipantDto[]>([])
2025-08-26 08:39:09 +00:00
const [localStream, setLocalStream] = useState<MediaStream>()
const [isAudioEnabled, setIsAudioEnabled] = useState(true)
const [isVideoEnabled, setIsVideoEnabled] = useState(true)
2025-08-29 09:37:38 +00:00
const [attendanceRecords, setAttendanceRecords] = useState<ClassroomAttendanceDto[]>([])
const [chatMessages, setChatMessages] = useState<ClassroomChatDto[]>([])
2025-08-26 08:39:09 +00:00
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,
2025-08-27 15:00:22 +00:00
autoMuteNewParticipants: true,
2025-08-26 08:39:09 +00:00
})
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 || [])
}
}
2025-08-27 20:55:01 +00:00
const fetchClassDetails = async () => {
const classEntity = await getClassroomById(params?.id ?? '')
if (classEntity) {
classEntity.data.scheduledStartTime = showDbDateAsIs(classEntity.data.scheduledStartTime)
setClassSession(classEntity.data)
2025-08-26 08:39:09 +00:00
}
2025-08-27 20:55:01 +00:00
}
useEffect(() => {
fetchClassDetails()
fetchClassChats()
fetchClassAttendances()
2025-08-26 08:39:09 +00:00
}, [])
2025-08-27 20:55:01 +00:00
useEffect(() => {
if (classSession.id) {
initializeServices()
return () => {
cleanup()
}
}
}, [classSession.id])
2025-08-26 08:39:09 +00:00
// Apply class settings
useEffect(() => {
2025-08-27 20:55:01 +00:00
if (classSession?.settingsDto) {
setClassSettings(classSession.settingsDto)
2025-08-26 08:39:09 +00:00
const selectedLayout =
2025-08-27 20:55:01 +00:00
layouts.find((l) => l.id === classSession.settingsDto!.defaultLayout) || layouts[0]
2025-08-26 08:39:09 +00:00
setCurrentLayout(selectedLayout)
// Apply default audio/video states for new participants
if (user.role === 'student') {
2025-08-27 20:55:01 +00:00
setIsAudioEnabled(classSession.settingsDto.defaultMicrophoneState === 'unmuted')
setIsVideoEnabled(classSession.settingsDto.defaultCameraState === 'on')
2025-08-26 08:39:09 +00:00
}
}
2025-08-27 20:55:01 +00:00
}, [classSession?.settingsDto, user.role])
2025-08-26 08:39:09 +00:00
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)))
})
// Setup SignalR event handlers
signalRServiceRef.current.setParticipantJoinHandler((userId, name) => {
2025-08-29 09:37:38 +00:00
// 🔑 Eğer gelen participant bizsek, listeye ekleme
2025-08-28 11:53:47 +00:00
if (userId === user.id) return
2025-08-29 09:37:38 +00:00
console.log(`Participant joined: ${name}`)
2025-08-26 08:39:09 +00:00
// Create WebRTC connection for new participant
if (webRTCServiceRef.current) {
webRTCServiceRef.current.createPeerConnection(userId)
}
setParticipants((prev) => {
const existing = prev.find((p) => p.id === userId)
if (existing) return prev
return [
...prev,
{
id: userId,
name,
isTeacher: false,
isAudioMuted: classSettings.autoMuteNewParticipants,
isVideoMuted: classSettings.defaultCameraState === 'off',
},
]
})
})
signalRServiceRef.current.setParticipantLeaveHandler((userId) => {
console.log(`Participant left: ${userId}`)
setParticipants((prev) => prev.filter((p) => p.id !== userId))
webRTCServiceRef.current?.closePeerConnection(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)),
)
2025-08-26 08:39:09 +00:00
})
signalRServiceRef.current.setHandRaiseDismissedHandler((studentId) => {
setParticipants((prev) =>
prev.map((p) => (p.id === studentId ? { ...p, isHandRaised: false } : p)),
2025-08-26 08:39:09 +00:00
)
// 👇 kendi stateini de sıfırla
if (studentId === user.id) {
setHasRaisedHand(false)
}
2025-08-26 08:39:09 +00:00
})
// Join the class
2025-08-29 09:37:38 +00:00
await signalRServiceRef.current.joinClass(
classSession.id,
user.id,
user.name,
user.role === 'teacher',
)
2025-08-26 08:39:09 +00:00
} catch (error) {
console.error('Failed to initialize services:', error)
}
}
const cleanup = async () => {
if (signalRServiceRef.current) {
2025-08-27 20:55:01 +00:00
await signalRServiceRef.current.leaveClass(classSession.id)
2025-08-26 08:39:09 +00:00
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 () => {
2025-08-28 11:53:47 +00:00
try {
// Eğer teacher ise sınıfı kapat
2025-08-29 19:04:53 +00:00
if (user.role === 'teacher' && user.id === classSession.teacherId) {
2025-08-28 11:53:47 +00:00
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)
}
2025-08-26 08:39:09 +00:00
}
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(),
2025-08-29 09:37:38 +00:00
user.role === 'teacher',
2025-08-26 08:39:09 +00:00
)
} else {
await signalRServiceRef.current.sendChatMessage(
classSession.id,
user.id,
user.name,
newMessage.trim(),
user.role === 'teacher',
)
}
setNewMessage('')
}
}
2025-08-29 09:37:38 +00:00
const handleMuteParticipant = async (
participantId: string,
isMuted: boolean,
isTeacher: boolean,
) => {
2025-08-26 08:39:09 +00:00
if (signalRServiceRef.current && user.role === 'teacher') {
2025-08-29 09:37:38 +00:00
await signalRServiceRef.current.muteParticipant(
classSession.id,
participantId,
isMuted,
isTeacher,
)
2025-08-26 08:39:09 +00:00
}
}
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,
2025-08-29 09:37:38 +00:00
user.role === 'teacher',
2025-08-26 08:39:09 +00:00
)
}
}
}
}
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 = {
2025-08-29 09:37:38 +00:00
id: crypto.randomUUID(),
2025-08-26 08:39:09 +00:00
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
2025-08-29 09:37:38 +00:00
const simulateStudentJoin = async () => {
2025-08-26 08:39:09 +00:00
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)]
2025-08-29 09:37:38 +00:00
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
)
2025-08-26 08:39:09 +00:00
}
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
2025-08-29 13:20:51 +00:00
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}
/>
)
2025-08-26 08:39:09 +00:00
2025-08-29 13:20:51 +00:00
case 'participants':
return (
<ParticipantsPanel
user={user}
participants={participants}
attendanceRecords={attendanceRecords}
onMuteParticipant={handleMuteParticipant}
onKickParticipant={handleKickParticipant}
onClose={() => setActiveSidePanel(null)}
formatTime={formatTime}
formatDuration={formatDuration}
/>
)
2025-08-26 08:39:09 +00:00
2025-08-29 13:20:51 +00:00
case 'documents':
return (
<DocumentsPanel
user={user}
documents={documents}
onUpload={handleUploadDocument}
onDelete={handleDeleteDocument}
onView={handleViewDocument}
onClose={() => setActiveSidePanel(null)}
formatFileSize={formatFileSize}
getFileIcon={getFileIcon}
/>
)
2025-08-26 08:39:09 +00:00
2025-08-29 13:20:51 +00:00
case 'layout':
return (
<LayoutPanel
layouts={layouts}
currentLayout={currentLayout}
onChangeLayout={handleLayoutChange}
onClose={() => setActiveSidePanel(null)}
/>
)
2025-08-26 08:39:09 +00:00
2025-08-29 13:20:51 +00:00
case 'settings':
return (
<SettingsPanel
user={user}
classSettings={classSettings}
onSettingsChange={handleSettingsChange}
onClose={() => setActiveSidePanel(null)}
/>
)
2025-08-26 08:39:09 +00:00
2025-08-29 13:20:51 +00:00
default:
return null
}
2025-08-26 08:39:09 +00:00
}
return (
2025-08-29 09:37:38 +00:00
<>
<Helmet
titleTemplate="%s | Kurs Platform"
title={translate('::' + 'App.Classroom.RoomDetail')}
2025-08-29 09:37:38 +00:00
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 */}
2025-08-26 08:39:09 +00:00
<div
2025-08-29 09:37:38 +00:00
className={`flex-1 flex flex-col min-h-0 h-full transition-all duration-300 ${!activeSidePanel ? 'flex items-center justify-center' : ''}`}
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
{/* 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
}
2025-08-26 08:39:09 +00:00
/>
</div>
</div>
</div>
2025-08-29 09:37:38 +00:00
{/* 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>
)}
2025-08-26 08:39:09 +00:00
</div>
2025-08-29 09:37:38 +00:00
{/* 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 */}
2025-08-26 08:39:09 +00:00
<button
2025-08-29 09:37:38 +00:00
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"
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
<FaPhone size={16} />
2025-08-26 08:39:09 +00:00
</button>
2025-08-29 09:37:38 +00:00
</div>
2025-08-26 08:39:09 +00:00
2025-08-29 09:37:38 +00:00
{/* Right Side - Panel Controls */}
<div className="flex items-center">
2025-08-26 08:39:09 +00:00
<button
2025-08-29 09:37:38 +00:00
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-white"
onClick={() => setMobileMenuOpen(true)}
aria-label="Menüyü Aç"
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
<FaBars size={20} />
2025-08-26 08:39:09 +00:00
</button>
2025-08-29 09:37:38 +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>
<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>
</>
2025-08-26 08:39:09 +00:00
)}
2025-08-29 09:37:38 +00:00
</div>
2025-08-26 08:39:09 +00:00
2025-08-29 09:37:38 +00:00
{/* 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 */}
2025-08-26 08:39:09 +00:00
<button
2025-08-29 09:37:38 +00:00
onClick={handleLeaveCall}
className="p-3 rounded-full bg-red-600 hover:bg-red-700 text-white transition-all"
title="Aramayı Sonlandır"
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
<FaPhone size={16} />
2025-08-26 08:39:09 +00:00
</button>
2025-08-29 09:37:38 +00:00
</div>
2025-08-26 08:39:09 +00:00
2025-08-29 09:37:38 +00:00
{/* 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 */}
2025-08-26 08:39:09 +00:00
<button
2025-08-29 09:37:38 +00:00
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'}
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
{isFullscreen ? <FaCompress size={14} /> : <FaExpand size={14} />}
2025-08-26 08:39:09 +00:00
</button>
2025-08-29 09:37:38 +00:00
{/* 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>
)}
2025-08-26 08:39:09 +00:00
2025-08-29 12:04:22 +00:00
{/* 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>
)}
2025-08-29 09:37:38 +00:00
{/* 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"
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
<FaUserFriends size={14} />
</button>
{/* Teacher Only Options */}
{user.role === 'teacher' && (
<>
{/* Documents Button */}
2025-08-26 08:39:09 +00:00
<button
2025-08-29 09:37:38 +00:00
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"
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
<FaFile size={14} />
2025-08-26 08:39:09 +00:00
</button>
2025-08-29 09:37:38 +00:00
{/* Mute All Button */}
2025-08-26 08:39:09 +00:00
<button
2025-08-29 09:37:38 +00:00
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'}
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
{isAllMuted ? <FaVolumeUp size={14} /> : <FaVolumeMute size={14} />}
2025-08-26 08:39:09 +00:00
</button>
2025-08-29 09:37:38 +00:00
{/* Add Student Demo Button */}
2025-08-26 08:39:09 +00:00
<button
2025-08-29 09:37:38 +00:00
onClick={simulateStudentJoin}
className="p-2 rounded-lg transition-all bg-gray-700 hover:bg-gray-600 text-white"
title="Öğrenci Ekle (Demo)"
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
<FaUserPlus size={14} />
2025-08-26 08:39:09 +00:00
</button>
2025-08-29 09:37:38 +00:00
</>
)}
2025-08-26 08:39:09 +00:00
2025-08-29 09:37:38 +00:00
{/* Layout Button */}
2025-08-26 08:39:09 +00:00
<button
2025-08-29 09:37:38 +00:00
onClick={() => toggleSidePanel('layout')}
className={`p-2 rounded-lg transition-all ${
activeSidePanel === 'layout'
? 'bg-blue-600 text-white'
2025-08-26 08:39:09 +00:00
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
2025-08-29 09:37:38 +00:00
title="Layout"
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
<FaLayerGroup size={14} />
2025-08-26 08:39:09 +00:00
</button>
2025-08-29 09:37:38 +00:00
{/* Settings Button */}
2025-08-26 08:39:09 +00:00
<button
2025-08-29 09:37:38 +00:00
onClick={() => toggleSidePanel('settings')}
className={`p-2 rounded-lg transition-all ${
activeSidePanel === 'settings'
2025-08-26 08:39:09 +00:00
? 'bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
2025-08-29 09:37:38 +00:00
title="Ayarlar"
2025-08-26 08:39:09 +00:00
>
2025-08-29 09:37:38 +00:00
<FaWrench size={14} />
2025-08-26 08:39:09 +00:00
</button>
2025-08-29 09:37:38 +00:00
</div>
2025-08-26 08:39:09 +00:00
</div>
</div>
2025-08-29 09:37:38 +00:00
{/* Kick Participant Modal */}
<KickParticipantModal
participant={kickingParticipant}
isOpen={!!kickingParticipant}
onClose={() => setKickingParticipant(null)}
onConfirm={handleKickParticipant}
/>
</motion.div>
</div>
</>
2025-08-26 08:39:09 +00:00
)
}
2025-08-26 14:57:09 +00:00
export default RoomDetail