Classroom güncellemeleri
This commit is contained in:
parent
277a5314cc
commit
09380b1e63
6 changed files with 93 additions and 281 deletions
|
|
@ -117,6 +117,7 @@ public class ClassroomHub : Hub
|
|||
[HubMethodName("JoinClass")]
|
||||
public async Task JoinClassAsync(Guid sessionId, Guid userId, string userName, bool isTeacher, bool isActive)
|
||||
{
|
||||
bool initialMuteState;
|
||||
var classroom = await _classSessionRepository.GetAsync(sessionId);
|
||||
if (classroom == null)
|
||||
{
|
||||
|
|
@ -128,6 +129,8 @@ public class ClassroomHub : Hub
|
|||
? new ClassroomSettingsDto()
|
||||
: JsonSerializer.Deserialize<ClassroomSettingsDto>(classroom.SettingsJson);
|
||||
|
||||
initialMuteState = !isTeacher && classroomSettings.AutoMuteNewParticipants ? true : classroomSettings.DefaultMicrophoneState == "muted";
|
||||
|
||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == sessionId && x.UserId == userId
|
||||
);
|
||||
|
|
@ -140,7 +143,7 @@ public class ClassroomHub : Hub
|
|||
userId,
|
||||
userName,
|
||||
isTeacher,
|
||||
classroomSettings.DefaultMicrophoneState == "muted",
|
||||
initialMuteState,
|
||||
classroomSettings.DefaultCameraState == "off",
|
||||
false,
|
||||
isActive
|
||||
|
|
@ -351,28 +354,49 @@ public class ClassroomHub : Hub
|
|||
[HubMethodName("KickParticipant")]
|
||||
public async Task KickParticipantAsync(Guid sessionId, Guid participantId)
|
||||
{
|
||||
var attendance = await _attendanceRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null
|
||||
);
|
||||
|
||||
if (attendance != null)
|
||||
try
|
||||
{
|
||||
await CloseAttendanceAsync(attendance);
|
||||
await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", attendance);
|
||||
// 1. Attendance kapat
|
||||
var attendance = await _attendanceRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null
|
||||
);
|
||||
|
||||
if (attendance != null)
|
||||
{
|
||||
await CloseAttendanceAsync(attendance);
|
||||
await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", attendance);
|
||||
}
|
||||
|
||||
// 2. Participant bul
|
||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == sessionId && x.UserId == participantId
|
||||
);
|
||||
|
||||
if (participant != null)
|
||||
{
|
||||
// Önce SignalR grubundan çıkar
|
||||
if (!string.IsNullOrEmpty(participant.ConnectionId))
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(participant.ConnectionId, sessionId.ToString());
|
||||
|
||||
// Kullanıcıya "zorunlu çıkış" sinyali gönder
|
||||
await Clients.Client(participant.ConnectionId)
|
||||
.SendAsync("ForceDisconnect", "You have been removed from the class.");
|
||||
}
|
||||
|
||||
// DB’de pasife al
|
||||
await DeactivateParticipantAsync(participant);
|
||||
}
|
||||
|
||||
// 3. Diğerlerine duyur
|
||||
_logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", participantId, sessionId);
|
||||
await Clients.Group(sessionId.ToString()).SendAsync("ParticipantLeft", participantId);
|
||||
}
|
||||
|
||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == sessionId && x.UserId == participantId
|
||||
);
|
||||
|
||||
if (participant != null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DeactivateParticipantAsync(participant);
|
||||
_logger.LogError(ex, "❌ KickParticipant hata verdi");
|
||||
await Clients.Caller.SendAsync("Error", "Kick işlemi başarısız oldu.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", participantId, sessionId);
|
||||
|
||||
await Clients.Group(sessionId.ToString()).SendAsync("ParticipantLeft", participantId);
|
||||
}
|
||||
|
||||
[HubMethodName("ApproveHandRaise")]
|
||||
|
|
|
|||
|
|
@ -264,21 +264,6 @@ export const ParticipantGrid: React.FC<ParticipantGridProps> = ({
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Expand button for non-main participants */}
|
||||
{!isMain && onParticipantFocus && (
|
||||
<div className="absolute top-2 right-2 z-10">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onParticipantFocus(participant.id)
|
||||
}}
|
||||
className="p-1 bg-black bg-opacity-50 text-white rounded-full hover:bg-opacity-70 transition-all"
|
||||
title="Büyüt"
|
||||
>
|
||||
<FaExpand size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import React, { useRef, useEffect } from 'react'
|
||||
import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa'
|
||||
import { ClassroomChatDto, ClassroomParticipantDto, MessageType } from '@/proxy/classroom/models'
|
||||
import {
|
||||
ClassroomChatDto,
|
||||
ClassroomParticipantDto,
|
||||
ClassroomSettingsDto,
|
||||
MessageType,
|
||||
} from '@/proxy/classroom/models'
|
||||
|
||||
interface ChatPanelProps {
|
||||
user: { id: string; name: string; role: string }
|
||||
|
|
@ -15,6 +20,7 @@ interface ChatPanelProps {
|
|||
onSendMessage: (e: React.FormEvent) => void
|
||||
onClose: () => void
|
||||
formatTime: (timestamp: string) => string
|
||||
classSettings: ClassroomSettingsDto
|
||||
}
|
||||
|
||||
const ChatPanel: React.FC<ChatPanelProps> = ({
|
||||
|
|
@ -30,6 +36,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
|
|||
onSendMessage,
|
||||
onClose,
|
||||
formatTime,
|
||||
classSettings,
|
||||
}) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
|
|
@ -67,18 +74,19 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
|
|||
<span>Herkese</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setMessageMode('private')}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
||||
messageMode === 'private'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaUser size={12} />
|
||||
<span>Özel</span>
|
||||
</button>
|
||||
|
||||
{classSettings.allowPrivateMessages && (
|
||||
<button
|
||||
onClick={() => setMessageMode('private')}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
||||
messageMode === 'private'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaUser size={12} />
|
||||
<span>Özel</span>
|
||||
</button>
|
||||
)}
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
import React from 'react'
|
||||
import { FaTimes } from 'react-icons/fa'
|
||||
import { ClassroomSettingsDto } from '@/proxy/classroom/models'
|
||||
|
||||
interface SettingsPanelProps {
|
||||
user: { role: string }
|
||||
classSettings: ClassroomSettingsDto
|
||||
onSettingsChange: (newSettings: Partial<ClassroomSettingsDto>) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const SettingsPanel: React.FC<SettingsPanelProps> = ({
|
||||
user,
|
||||
classSettings,
|
||||
onSettingsChange,
|
||||
onClose,
|
||||
}) => {
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Sınıf Ayarları</h3>
|
||||
<button onClick={onClose}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="space-y-6">
|
||||
{/* Katılımcı Davranışları */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-800 mb-3">Katılımcı İzinleri</h4>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700">Parmak kaldırma izni</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={classSettings.allowHandRaise}
|
||||
onChange={(e) => onSettingsChange({ allowHandRaise: e.target.checked })}
|
||||
className="rounded"
|
||||
disabled={user.role !== 'teacher'}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700">Öğrenci sohbet izni</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={classSettings.allowStudentChat}
|
||||
onChange={(e) => onSettingsChange({ allowStudentChat: e.target.checked })}
|
||||
className="rounded"
|
||||
disabled={user.role !== 'teacher'}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">Özel mesaj izni</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={classSettings.allowPrivateMessages}
|
||||
onChange={(e) => onSettingsChange({ allowPrivateMessages: e.target.checked })}
|
||||
className="rounded"
|
||||
disabled={user.role !== 'teacher'}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">Öğrenci ekran paylaşımı</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={classSettings.allowStudentScreenShare}
|
||||
onChange={(e) => onSettingsChange({ allowStudentScreenShare: e.target.checked })}
|
||||
className="rounded"
|
||||
disabled={user.role !== 'teacher'}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Varsayılan Ayarlar */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-800 mb-3">Varsayılan Ayarlar</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Varsayılan mikrofon durumu
|
||||
</label>
|
||||
<select
|
||||
value={classSettings.defaultMicrophoneState}
|
||||
onChange={(e) =>
|
||||
onSettingsChange({
|
||||
defaultMicrophoneState: e.target.value as 'muted' | 'unmuted',
|
||||
})
|
||||
}
|
||||
className="w-full p-2 border border-gray-300 rounded-md text-sm"
|
||||
disabled={user.role !== 'teacher'}
|
||||
>
|
||||
<option value="muted">Kapalı</option>
|
||||
<option value="unmuted">Açık</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Varsayılan kamera durumu
|
||||
</label>
|
||||
<select
|
||||
value={classSettings.defaultCameraState}
|
||||
onChange={(e) =>
|
||||
onSettingsChange({
|
||||
defaultCameraState: e.target.value as 'on' | 'off',
|
||||
})
|
||||
}
|
||||
className="w-full p-2 border border-gray-300 rounded-md text-sm"
|
||||
disabled={user.role !== 'teacher'}
|
||||
>
|
||||
<option value="on">Açık</option>
|
||||
<option value="off">Kapalı</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Varsayılan layout
|
||||
</label>
|
||||
<select
|
||||
value={classSettings.defaultLayout}
|
||||
onChange={(e) => onSettingsChange({ defaultLayout: e.target.value })}
|
||||
className="w-full p-2 border border-gray-300 rounded-md text-sm"
|
||||
disabled={user.role !== 'teacher'}
|
||||
>
|
||||
<option value="grid">Izgara Görünümü</option>
|
||||
<option value="sidebar">Sunum Modu</option>
|
||||
<option value="teacher-focus">Öğretmen Odaklı</option>
|
||||
<option value="interview">Karşılıklı Görüşme</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Otomatik Ayarlar */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-800 mb-3">Otomatik Ayarlar</h4>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700">Yeni katılımcıları otomatik sustur</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={classSettings.autoMuteNewParticipants}
|
||||
onChange={(e) => onSettingsChange({ autoMuteNewParticipants: e.target.checked })}
|
||||
className="rounded"
|
||||
disabled={user.role !== 'teacher'}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{user.role !== 'teacher' && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||
<p className="text-sm text-yellow-800">⚠️ Ayarları sadece öğretmen değiştirebilir.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsPanel
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
import { ClassroomAttendanceDto, ClassroomChatDto, HandRaiseDto, MessageType } from '@/proxy/classroom/models'
|
||||
import {
|
||||
ClassroomAttendanceDto,
|
||||
ClassroomChatDto,
|
||||
HandRaiseDto,
|
||||
MessageType,
|
||||
} from '@/proxy/classroom/models'
|
||||
import { store } from '@/store/store'
|
||||
import * as signalR from '@microsoft/signalr'
|
||||
|
||||
|
|
@ -62,6 +67,7 @@ export class SignalRService {
|
|||
this.connection.on('ParticipantMuted', (userId: string, isMuted: boolean) => {
|
||||
this.onParticipantMuted?.(userId, isMuted)
|
||||
})
|
||||
|
||||
|
||||
this.connection.on('HandRaiseReceived', (payload: any) => {
|
||||
// payload = { handRaiseId, studentId, studentName, ... }
|
||||
|
|
@ -101,6 +107,13 @@ export class SignalRService {
|
|||
this.connection.on('Error', (message: string) => {
|
||||
console.error('Hub error:', message)
|
||||
})
|
||||
|
||||
this.connection.on('ForceDisconnect', async (message: string) => {
|
||||
console.warn('⚠️ ForceDisconnect received:', message)
|
||||
|
||||
await this.disconnect()
|
||||
window.location.href = '/classrooms'
|
||||
})
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
|
|
@ -211,7 +224,7 @@ export class SignalRService {
|
|||
message: string,
|
||||
recipientId: string,
|
||||
recipientName: string,
|
||||
isTeacher: boolean
|
||||
isTeacher: boolean,
|
||||
): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
console.log(
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ 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'
|
||||
|
||||
|
|
@ -110,12 +109,8 @@ const RoomDetail: React.FC = () => {
|
|||
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 [currentLayout, setCurrentLayout] = useState<VideoLayoutDto | null>(null)
|
||||
|
||||
const [focusedParticipant, setFocusedParticipant] = useState<string>()
|
||||
const [hasRaisedHand, setHasRaisedHand] = useState(false)
|
||||
const [isAllMuted, setIsAllMuted] = useState(false)
|
||||
|
|
@ -402,10 +397,16 @@ const RoomDetail: React.FC = () => {
|
|||
setChatMessages((prev) => [...prev, message])
|
||||
})
|
||||
|
||||
signalRServiceRef.current.setParticipantMutedHandler((userId, isMuted) => {
|
||||
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
|
||||
|
|
@ -737,6 +738,7 @@ const RoomDetail: React.FC = () => {
|
|||
onSendMessage={handleSendMessage}
|
||||
onClose={() => setActiveSidePanel(null)}
|
||||
formatTime={formatTime}
|
||||
classSettings={classSettings}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
@ -775,22 +777,12 @@ const RoomDetail: React.FC = () => {
|
|||
return (
|
||||
<LayoutPanel
|
||||
layouts={layouts}
|
||||
currentLayout={currentLayout}
|
||||
currentLayout={currentLayout ?? layouts[0]}
|
||||
onChangeLayout={handleLayoutChange}
|
||||
onClose={() => setActiveSidePanel(null)}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'settings':
|
||||
return (
|
||||
<SettingsPanel
|
||||
user={user}
|
||||
classSettings={classSettings}
|
||||
onSettingsChange={handleSettingsChange}
|
||||
onClose={() => setActiveSidePanel(null)}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
|
@ -857,7 +849,7 @@ const RoomDetail: React.FC = () => {
|
|||
onToggleVideo={user.role === 'observer' ? () => {} : handleToggleVideo}
|
||||
onLeaveCall={handleLeaveCall}
|
||||
onMuteParticipant={handleMuteParticipant}
|
||||
layout={currentLayout}
|
||||
layout={currentLayout ?? layouts[0]}
|
||||
focusedParticipant={focusedParticipant}
|
||||
onParticipantFocus={handleParticipantFocus}
|
||||
hasSidePanel={!!activeSidePanel}
|
||||
|
|
@ -1021,13 +1013,9 @@ const RoomDetail: React.FC = () => {
|
|||
>
|
||||
<FaUserFriends />
|
||||
<span>Katılımcılar</span>
|
||||
{/* Katılımcı adedi badge */}
|
||||
<span className="absolute top-2 right-3 bg-blue-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
|
||||
{participants.length + 1}
|
||||
</span>
|
||||
{/* El kaldıran badge */}
|
||||
{raisedHandsCount > 0 && (
|
||||
<span className="absolute top-2 right-8 bg-yellow-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
|
||||
<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>
|
||||
)}
|
||||
|
|
@ -1050,15 +1038,6 @@ const RoomDetail: React.FC = () => {
|
|||
>
|
||||
<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={() => {
|
||||
|
|
@ -1205,22 +1184,6 @@ const RoomDetail: React.FC = () => {
|
|||
</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')}
|
||||
|
|
@ -1232,13 +1195,9 @@ const RoomDetail: React.FC = () => {
|
|||
title="Katılımcılar"
|
||||
>
|
||||
<FaUserFriends size={14} />
|
||||
{/* Katılımcı adedi badge */}
|
||||
<span className="absolute -top-1 -right-1 bg-blue-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
|
||||
{participants.length + 1}
|
||||
</span>
|
||||
{/* El kaldıran badge */}
|
||||
{raisedHandsCount > 0 && (
|
||||
<span className="absolute -top-1 -right-6 bg-yellow-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
|
||||
<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>
|
||||
)}
|
||||
|
|
@ -1283,19 +1242,6 @@ const RoomDetail: React.FC = () => {
|
|||
>
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue