import { lazy, Suspense, useMemo, type KeyboardEvent } from 'react'
import { FaMicrophoneSlash, FaUserTimes } from 'react-icons/fa'
import { VideoroomParticipantDto, VideoroomLayoutDto } from '@/proxy/videoroom/models'
import { Button } from '@/components/ui'
const VideoPlayer = lazy(() =>
import('./VideoPlayer').then((module) => ({ default: module.VideoPlayer })),
)
interface RoomParticipantProps {
participants: VideoroomParticipantDto[]
localStream?: MediaStream | null
currentUserId: string
currentUserName: string
isTeacher: boolean
isAudioEnabled: boolean
isVideoEnabled: boolean
onMuteParticipant?: (participantId: string, isMuted: boolean, isTeacher: boolean) => void
layout?: VideoroomLayoutDto
focusedParticipant?: string
onParticipantFocus?: (participantId: string | undefined) => void
onKickParticipant?: (participantId: string) => void
hasSidePanel?: boolean
}
/** Katılımcı sayısına göre kolon sayısı. Satırlar `auto-rows-fr` ile eşit yüksekliğe dağıtılır. */
const getGridColumns = (count: number) => {
if (count <= 1) return 'grid-cols-1'
if (count <= 2) return 'grid-cols-1 sm:grid-cols-2'
if (count <= 4) return 'grid-cols-2 sm:grid-cols-2'
if (count <= 9) return 'grid-cols-2 sm:grid-cols-3'
return 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
}
const VideoTileFallback = () =>
export const RoomParticipant = ({
participants,
localStream,
currentUserId,
currentUserName,
isTeacher,
isAudioEnabled,
isVideoEnabled,
onMuteParticipant,
layout,
focusedParticipant,
onParticipantFocus,
onKickParticipant,
hasSidePanel = false,
}: RoomParticipantProps) => {
const allParticipants = useMemo(() => {
const remote = (participants ?? []).filter((p) => p.id !== currentUserId)
const self: VideoroomParticipantDto = {
id: currentUserId,
name: currentUserName,
sessionId: remote[0]?.sessionId ?? '',
isTeacher,
isAudioMuted: !isAudioEnabled,
isVideoMuted: !isVideoEnabled,
isActive: true,
stream: localStream ?? undefined,
}
return [self, ...remote]
}, [
participants,
currentUserId,
currentUserName,
isTeacher,
isAudioEnabled,
isVideoEnabled,
localStream,
])
const renderParticipant = (
participant: VideoroomParticipantDto,
{ isMain = false }: { isMain?: boolean } = {},
) => {
const isSelf = participant.id === currentUserId
const isFocusable = !isMain && !!onParticipantFocus
const canModerate = isTeacher && !isSelf
return (
onParticipantFocus?.(participant.id),
onKeyDown: (event: KeyboardEvent
) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
onParticipantFocus?.(participant.id)
}
},
}
: {})}
>
}>
{participant.isHandRaised && !isSelf && (
✋
)}
{canModerate && (
{
e.stopPropagation()
onMuteParticipant?.(participant.id, !participant.isAudioMuted, isTeacher)
}}
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-1 text-xs text-white ${
participant.isAudioMuted ? '!bg-red-600' : '!bg-gray-600 hover:!bg-gray-700'
} transition-colors`}
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
>
{onKickParticipant && (
{
e.stopPropagation()
onKickParticipant(participant.id)
}}
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-red-600 !p-1 text-xs text-white transition-colors hover:!bg-red-700"
title="Sınıftan Çıkar"
>
)}
)}
)
}
const renderGridLayout = () => {
const count = allParticipants.length
const gap = count <= 4 ? 'gap-2 sm:gap-3' : 'gap-1 sm:gap-2'
const padding = count === 1 ? 'p-0' : count <= 4 ? 'p-2 sm:p-4' : 'p-1 sm:p-2'
return (
{allParticipants.map((participant, index) => (
2 ? 'col-span-2 sm:col-span-1' : ''
}`}
>
{renderParticipant(participant)}
))}
)
}
const renderSidebarLayout = () => {
const mainParticipant =
allParticipants.find((p) => p.id === focusedParticipant) ?? allParticipants[0]
const otherParticipants = allParticipants.filter((p) => p.id !== mainParticipant.id)
const sidebarWidth = hasSidePanel
? 'w-20 sm:w-24 md:w-32 lg:w-40'
: 'w-24 sm:w-32 md:w-40 lg:w-48'
return (
{renderParticipant(mainParticipant, { isMain: true })}
{otherParticipants.length > 0 && (
{otherParticipants.map((participant) => (
{renderParticipant(participant)}
))}
)}
)
}
const renderTeacherFocusLayout = () => {
const teacher = allParticipants.find((p) => p.isTeacher) ?? allParticipants[0]
return (
{renderParticipant(teacher, { isMain: true })}
)
}
// Ne yerel yayın ne de katılımcı varsa gösterilecek bir şey yok
if (!localStream && allParticipants.length <= 1) {
return null
}
switch (layout?.type) {
case 'sidebar':
return {renderSidebarLayout()}
case 'teacher-focus':
return {renderTeacherFocusLayout()}
default:
return {renderGridLayout()}
}
}