RoomDetail komponentlere ayrıldı
This commit is contained in:
parent
823ee98384
commit
18b65e6c8a
7 changed files with 905 additions and 811 deletions
|
|
@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
|||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.48gll4p3s3o"
|
||||
"revision": "0.d148klvmpj8"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
|
|
|||
188
ui/src/components/classroom/ChatPanel.tsx
Normal file
188
ui/src/components/classroom/ChatPanel.tsx
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import React, { useRef, useEffect } from 'react'
|
||||
import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa'
|
||||
import { ClassroomChatDto, ClassroomParticipantDto } from '@/proxy/classroom/models'
|
||||
|
||||
interface ChatPanelProps {
|
||||
user: { id: string; name: string; role: string }
|
||||
participants: ClassroomParticipantDto[]
|
||||
chatMessages: ClassroomChatDto[]
|
||||
newMessage: string
|
||||
setNewMessage: (msg: string) => void
|
||||
messageMode: 'public' | 'private' | 'announcement'
|
||||
setMessageMode: (mode: 'public' | 'private' | 'announcement') => void
|
||||
selectedRecipient: { id: string; name: string } | null
|
||||
setSelectedRecipient: (recipient: { id: string; name: string } | null) => void
|
||||
onSendMessage: (e: React.FormEvent) => void
|
||||
onClose: () => void
|
||||
formatTime: (timestamp: string) => string
|
||||
}
|
||||
|
||||
const ChatPanel: React.FC<ChatPanelProps> = ({
|
||||
user,
|
||||
participants,
|
||||
chatMessages,
|
||||
newMessage,
|
||||
setNewMessage,
|
||||
messageMode,
|
||||
setMessageMode,
|
||||
selectedRecipient,
|
||||
setSelectedRecipient,
|
||||
onSendMessage,
|
||||
onClose,
|
||||
formatTime,
|
||||
}) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [chatMessages])
|
||||
|
||||
const availableRecipients = participants.filter((p) => p.id !== user.id)
|
||||
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
{/* Header */}
|
||||
<div className="p-3 sm:p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 className="text-base sm:text-lg font-semibold">Sohbet</h3>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded lg:hidden">
|
||||
<FaTimes className="text-gray-500" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mesaj Modu */}
|
||||
<div className="p-3 border-b border-gray-200 bg-gray-50">
|
||||
<div className="flex space-x-2 mb-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setMessageMode('public')
|
||||
setSelectedRecipient(null)
|
||||
}}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
||||
messageMode === 'public'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaUsers size={12} />
|
||||
<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>
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setMessageMode('announcement')
|
||||
setSelectedRecipient(null)
|
||||
}}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
||||
messageMode === 'announcement'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaBullhorn size={12} />
|
||||
<span>Duyuru</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{messageMode === 'private' && (
|
||||
<select
|
||||
value={selectedRecipient?.id || ''}
|
||||
onChange={(e) => {
|
||||
const recipient = availableRecipients.find((p) => p.id === e.target.value)
|
||||
setSelectedRecipient(recipient ? { id: recipient.id, name: recipient.name } : null)
|
||||
}}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded"
|
||||
>
|
||||
<option value="">Kişi seçin...</option>
|
||||
{availableRecipients.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name} {p.isTeacher ? '(Öğretmen)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mesaj Listesi */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{chatMessages.length === 0 ? (
|
||||
<div className="text-center text-gray-500 text-sm">Henüz mesaj yok.</div>
|
||||
) : (
|
||||
chatMessages.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
className={`${
|
||||
m.messageType === 'announcement'
|
||||
? 'w-full'
|
||||
: m.senderId === user.id
|
||||
? 'flex justify-end'
|
||||
: 'flex justify-start'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-xs px-3 py-2 rounded-lg ${
|
||||
m.messageType === 'announcement'
|
||||
? 'bg-red-100 text-red-800 border border-red-200 w-full text-center'
|
||||
: m.messageType === 'private'
|
||||
? m.senderId === user.id
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-green-100 text-green-800 border'
|
||||
: m.senderId === user.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: m.isTeacher
|
||||
? 'bg-yellow-100 text-yellow-800 border'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{m.senderId !== user.id && (
|
||||
<div className="text-xs font-semibold mb-1">
|
||||
{m.senderName}
|
||||
{m.isTeacher && ' (Öğretmen)'}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm">{m.message}</div>
|
||||
<div className="text-xs mt-1 opacity-75">{formatTime(m.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Mesaj Gönderme */}
|
||||
<form onSubmit={onSendMessage} className="p-4 border-t border-gray-200">
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="Mesaj yaz..."
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newMessage.trim()}
|
||||
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
<FaPaperPlane size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatPanel
|
||||
153
ui/src/components/classroom/DocumentsPanel.tsx
Normal file
153
ui/src/components/classroom/DocumentsPanel.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
import { FaTimes, FaFile, FaEye, FaDownload, FaTrash } from 'react-icons/fa'
|
||||
import { ClassDocumentDto } from '@/proxy/classroom/models'
|
||||
|
||||
interface DocumentsPanelProps {
|
||||
user: { role: string; name: string }
|
||||
documents: ClassDocumentDto[]
|
||||
onUpload: (file: File) => void
|
||||
onDelete: (id: string) => void
|
||||
onView: (doc: ClassDocumentDto) => void
|
||||
onClose: () => void
|
||||
formatFileSize: (bytes: number) => string
|
||||
getFileIcon: (type: string) => JSX.Element
|
||||
}
|
||||
|
||||
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
user,
|
||||
documents,
|
||||
onUpload,
|
||||
onDelete,
|
||||
onView,
|
||||
onClose,
|
||||
formatFileSize,
|
||||
getFileIcon,
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [dragOver, setDragOver] = useState(false)
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
if (user.role !== 'teacher') return
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
files.forEach((file) => onUpload(file))
|
||||
}
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (user.role !== 'teacher') return
|
||||
const files = Array.from(e.target.files || [])
|
||||
files.forEach((file) => onUpload(file))
|
||||
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
{/* Header */}
|
||||
<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 Dokümanları</h3>
|
||||
<button onClick={onClose}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{/* Upload Area (Teacher Only) */}
|
||||
{user.role === 'teacher' && (
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-6 mb-4 text-center transition-colors ${
|
||||
dragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
}}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
>
|
||||
<FaFile size={32} className="mx-auto text-gray-400 mb-2" />
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Doküman Yükle</p>
|
||||
<p className="text-xs text-gray-500 mb-3">Dosyaları buraya sürükleyin veya seçin</p>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Dosya Seç
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
accept=".pdf,.doc,.docx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.odp"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documents List */}
|
||||
{documents.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FaFile size={32} className="mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-sm">Henüz doküman yüklenmemiş.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{documents.map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:shadow-sm transition-shadow"
|
||||
>
|
||||
<div className="flex items-center space-x-3 min-w-0 flex-1">
|
||||
<div className="text-lg flex-shrink-0">{getFileIcon(doc.type)}</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-medium text-gray-800 text-sm truncate">{doc.name}</h4>
|
||||
<p className="text-xs text-gray-600">
|
||||
{formatFileSize(doc.size)} •{' '}
|
||||
{new Date(doc.uploadedAt).toLocaleDateString('tr-TR')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{doc.uploadedBy}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => onView(doc)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye size={12} />
|
||||
</button>
|
||||
|
||||
<a
|
||||
href={doc.url}
|
||||
download={doc.name}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded transition-colors"
|
||||
title="İndir"
|
||||
>
|
||||
<FaDownload size={12} />
|
||||
</a>
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => onDelete(doc.id)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentsPanel
|
||||
112
ui/src/components/classroom/LayoutPanel.tsx
Normal file
112
ui/src/components/classroom/LayoutPanel.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import React from 'react'
|
||||
import { FaTimes, FaTh, FaExpand, FaDesktop, FaUsers } from 'react-icons/fa'
|
||||
import { VideoLayoutDto } from '@/proxy/classroom/models'
|
||||
|
||||
interface LayoutPanelProps {
|
||||
layouts: VideoLayoutDto[]
|
||||
currentLayout: VideoLayoutDto
|
||||
onChangeLayout: (layout: VideoLayoutDto) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
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} />
|
||||
case 'teacher-focus':
|
||||
return <FaDesktop size={24} />
|
||||
default:
|
||||
return <FaTh size={24} />
|
||||
}
|
||||
}
|
||||
|
||||
const LayoutPanel: React.FC<LayoutPanelProps> = ({
|
||||
layouts,
|
||||
currentLayout,
|
||||
onChangeLayout,
|
||||
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">Video Layout Seçin</h3>
|
||||
<button onClick={onClose}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
<div className="space-y-3">
|
||||
{layouts.map((layout) => (
|
||||
<button
|
||||
key={layout.id}
|
||||
onClick={() => onChangeLayout(layout)}
|
||||
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
|
||||
currentLayout.id === layout.id
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<div
|
||||
className={`p-2 rounded-full ${
|
||||
currentLayout.id === layout.id
|
||||
? 'bg-blue-100 text-blue-600'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{getLayoutIcon(layout.type)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 text-sm">{layout.name}</h4>
|
||||
<p className="text-xs text-gray-600">{layout.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layout Preview */}
|
||||
<div className="bg-gray-100 rounded p-3 h-16 flex items-center justify-center">
|
||||
{layout.type === 'grid' && (
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="w-4 h-3 bg-blue-300 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{layout.type === 'sidebar' && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-6 bg-blue-500 rounded"></div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{layout.type === 'teacher-focus' && (
|
||||
<div className="space-y-1">
|
||||
<div className="w-12 h-4 bg-green-500 rounded"></div>
|
||||
<div className="flex space-x-1">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutPanel
|
||||
218
ui/src/components/classroom/ParticipantsPanel.tsx
Normal file
218
ui/src/components/classroom/ParticipantsPanel.tsx
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import React, { useState } from 'react'
|
||||
import {
|
||||
FaTimes,
|
||||
FaUsers,
|
||||
FaClipboardList,
|
||||
FaHandPaper,
|
||||
FaCheck,
|
||||
FaMicrophone,
|
||||
FaMicrophoneSlash,
|
||||
FaVideoSlash,
|
||||
FaUserTimes,
|
||||
} from 'react-icons/fa'
|
||||
import { ClassroomParticipantDto, ClassroomAttendanceDto } from '@/proxy/classroom/models'
|
||||
|
||||
interface ParticipantsPanelProps {
|
||||
user: { id: string; name: string; role: string }
|
||||
participants: ClassroomParticipantDto[]
|
||||
attendanceRecords: ClassroomAttendanceDto[]
|
||||
onMuteParticipant: (participantId: string, isMuted: boolean, isTeacher: boolean) => void
|
||||
onKickParticipant: (participantId: string) => void
|
||||
onClose: () => void
|
||||
formatTime: (timestamp: string) => string
|
||||
formatDuration: (minutes: number) => string
|
||||
}
|
||||
|
||||
const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
||||
user,
|
||||
participants,
|
||||
attendanceRecords,
|
||||
onMuteParticipant,
|
||||
onKickParticipant,
|
||||
onClose,
|
||||
formatTime,
|
||||
formatDuration,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'participants' | 'attendance'>('participants')
|
||||
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Katılımcılar ({participants.length + 1})
|
||||
</h3>
|
||||
<button onClick={onClose}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex space-x-1 bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('participants')}
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all ${
|
||||
activeTab === 'participants'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<FaUsers className="inline mr-1" size={14} />
|
||||
Katılımcılar
|
||||
</button>
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => setActiveTab('attendance')}
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all ${
|
||||
activeTab === 'attendance'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<FaClipboardList className="inline mr-1" size={14} />
|
||||
Katılım Raporu
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Participants Tab */}
|
||||
{activeTab === 'participants' && (
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
<div className="space-y-2">
|
||||
{/* Current User */}
|
||||
<div className="flex items-center justify-between p-2 rounded-lg bg-blue-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-semibold">
|
||||
{user.name.charAt(0)}
|
||||
</div>
|
||||
<span className="text-gray-900">{user.name} (Siz)</span>
|
||||
</div>
|
||||
{user.role === 'teacher' && (
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
|
||||
Öğretmen
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Other Participants */}
|
||||
{participants.map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="flex items-center justify-between p-2 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-gray-500 rounded-full flex items-center justify-center text-white text-sm font-semibold">
|
||||
{participant.name.charAt(0)}
|
||||
</div>
|
||||
<span className="text-gray-900">{participant.name}</span>
|
||||
|
||||
{/* Hand Raise Indicator */}
|
||||
{participant.isHandRaised && (
|
||||
<FaHandPaper className="text-yellow-600 ml-2" title="Parmak kaldırdı" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
{/* Hand Raise Controls (Teacher Only) */}
|
||||
{user.role === 'teacher' &&
|
||||
!participant.isTeacher &&
|
||||
participant.isHandRaised && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => console.log('approveHandRaise', participant.id)}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded transition-colors"
|
||||
title="El kaldırmayı onayla"
|
||||
>
|
||||
<FaCheck size={12} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => console.log('dismissHandRaise', participant.id)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="El kaldırmayı reddet"
|
||||
>
|
||||
<FaTimes size={12} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Mute / Unmute Button */}
|
||||
{user.role === 'teacher' && !participant.isTeacher && (
|
||||
<button
|
||||
onClick={() =>
|
||||
onMuteParticipant(participant.id, !participant.isAudioMuted, true)
|
||||
}
|
||||
className={`p-1 rounded transition-colors ${
|
||||
participant.isAudioMuted
|
||||
? 'text-green-600 hover:bg-green-50'
|
||||
: 'text-yellow-600 hover:bg-yellow-50'
|
||||
}`}
|
||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||
>
|
||||
{participant.isAudioMuted ? <FaMicrophone /> : <FaMicrophoneSlash />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Video muted indicator */}
|
||||
{participant.isVideoMuted && <FaVideoSlash className="text-red-500 text-sm" />}
|
||||
|
||||
{participant.isTeacher && (
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
|
||||
Öğretmen
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Kick Button (Teacher Only) */}
|
||||
{user.role === 'teacher' && !participant.isTeacher && (
|
||||
<button
|
||||
onClick={() => onKickParticipant(participant.id)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="Sınıftan Çıkar"
|
||||
>
|
||||
<FaUserTimes size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Attendance Tab */}
|
||||
{activeTab === 'attendance' && (
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{attendanceRecords.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FaClipboardList size={32} className="mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-sm text-gray-600">Henüz katılım kaydı bulunmamaktadır.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{attendanceRecords.map((record) => (
|
||||
<div key={record.id} className="p-3 border border-gray-200 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-gray-800">{record.studentName}</h4>
|
||||
<span className="text-sm font-semibold text-blue-600">
|
||||
{formatDuration(record.totalDurationMinutes)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 space-y-1">
|
||||
<div>Giriş: {formatTime(record.joinTime)}</div>
|
||||
<div>
|
||||
Çıkış: {record.leaveTime ? formatTime(record.leaveTime) : 'Devam ediyor'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ParticipantsPanel
|
||||
164
ui/src/components/classroom/SettingsPanel.tsx
Normal file
164
ui/src/components/classroom/SettingsPanel.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
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
|
||||
|
|
@ -63,6 +63,11 @@ 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/ChatPanel'
|
||||
import ParticipantsPanel from '@/components/classroom/ParticipantsPanel'
|
||||
import DocumentsPanel from '@/components/classroom/DocumentsPanel'
|
||||
import LayoutPanel from '@/components/classroom/LayoutPanel'
|
||||
import SettingsPanel from '@/components/classroom/SettingsPanel'
|
||||
|
||||
type SidePanelType =
|
||||
| 'chat'
|
||||
|
|
@ -659,822 +664,76 @@ const RoomDetail: React.FC = () => {
|
|||
const renderSidePanel = () => {
|
||||
if (!activeSidePanel) return null
|
||||
|
||||
const panelContent = () => {
|
||||
switch (activeSidePanel) {
|
||||
case 'chat': {
|
||||
const availableRecipients = participants.filter((p) => p.id !== user.id)
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
<div className="p-3 sm:p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900">
|
||||
Sınıf Sohbeti
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setActiveSidePanel(null)}
|
||||
className="p-1 hover:bg-gray-100 rounded lg:hidden"
|
||||
>
|
||||
<FaTimes className="text-gray-500" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
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}
|
||||
/>
|
||||
)
|
||||
|
||||
{/* Message Mode Selector */}
|
||||
<div className="p-3 border-b border-gray-200 bg-gray-50">
|
||||
<div className="flex space-x-2 mb-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setMessageMode('public')
|
||||
setSelectedRecipient(null)
|
||||
}}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
||||
messageMode === 'public'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaUsers size={12} />
|
||||
<span>Herkese</span>
|
||||
</button>
|
||||
case 'participants':
|
||||
return (
|
||||
<ParticipantsPanel
|
||||
user={user}
|
||||
participants={participants}
|
||||
attendanceRecords={attendanceRecords}
|
||||
onMuteParticipant={handleMuteParticipant}
|
||||
onKickParticipant={handleKickParticipant}
|
||||
onClose={() => setActiveSidePanel(null)}
|
||||
formatTime={formatTime}
|
||||
formatDuration={formatDuration}
|
||||
/>
|
||||
)
|
||||
|
||||
<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>
|
||||
case 'documents':
|
||||
return (
|
||||
<DocumentsPanel
|
||||
user={user}
|
||||
documents={documents}
|
||||
onUpload={handleUploadDocument}
|
||||
onDelete={handleDeleteDocument}
|
||||
onView={handleViewDocument}
|
||||
onClose={() => setActiveSidePanel(null)}
|
||||
formatFileSize={formatFileSize}
|
||||
getFileIcon={getFileIcon}
|
||||
/>
|
||||
)
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setMessageMode('announcement')
|
||||
setSelectedRecipient(null)
|
||||
}}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
||||
messageMode === 'announcement'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaBullhorn size={12} />
|
||||
<span>Duyuru</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
case 'layout':
|
||||
return (
|
||||
<LayoutPanel
|
||||
layouts={layouts}
|
||||
currentLayout={currentLayout}
|
||||
onChangeLayout={handleLayoutChange}
|
||||
onClose={() => setActiveSidePanel(null)}
|
||||
/>
|
||||
)
|
||||
|
||||
{messageMode === 'private' && (
|
||||
<select
|
||||
value={selectedRecipient?.id || ''}
|
||||
onChange={(e) => {
|
||||
const recipient = availableRecipients.find((p) => p.id === e.target.value)
|
||||
setSelectedRecipient(
|
||||
recipient ? { id: recipient.id, name: recipient.name } : null,
|
||||
)
|
||||
}}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded"
|
||||
>
|
||||
<option value="">Kişi seçin...</option>
|
||||
{availableRecipients.map((participant) => (
|
||||
<option key={participant.id} value={participant.id}>
|
||||
{participant.name} {participant.isTeacher ? '(Öğretmen)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
case 'settings':
|
||||
return (
|
||||
<SettingsPanel
|
||||
user={user}
|
||||
classSettings={classSettings}
|
||||
onSettingsChange={handleSettingsChange}
|
||||
onClose={() => setActiveSidePanel(null)}
|
||||
/>
|
||||
)
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{chatMessages.length === 0 ? (
|
||||
<div className="text-center text-gray-500 text-sm">
|
||||
Henüz mesaj bulunmamaktadır.
|
||||
</div>
|
||||
) : (
|
||||
chatMessages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`${
|
||||
message.messageType === 'announcement'
|
||||
? 'w-full'
|
||||
: message.senderId === user.id
|
||||
? 'flex justify-end'
|
||||
: 'flex justify-start'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-xs px-3 py-2 rounded-lg ${
|
||||
message.messageType === 'announcement'
|
||||
? 'bg-red-100 text-red-800 border border-red-200 w-full text-center'
|
||||
: message.messageType === 'private'
|
||||
? message.senderId === user.id
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-green-100 text-green-800 border border-green-200'
|
||||
: message.senderId === user.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: message.isTeacher
|
||||
? 'bg-yellow-100 text-yellow-800 border border-yellow-200'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{message.senderId !== user.id && (
|
||||
<div className="text-xs font-semibold mb-1">
|
||||
{message.senderName}
|
||||
{message.isTeacher && ' (Öğretmen)'}
|
||||
{message.messageType === 'private' &&
|
||||
message.recipientId === user.id &&
|
||||
' (Size özel)'}
|
||||
</div>
|
||||
)}
|
||||
{message.messageType === 'private' && message.senderId === user.id && (
|
||||
<div className="text-xs mb-1 opacity-75">→ {message.recipientName}</div>
|
||||
)}
|
||||
{message.messageType === 'announcement' && (
|
||||
<div className="text-xs font-semibold mb-1">
|
||||
📢 DUYURU - {message.senderName}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm">{message.message}</div>
|
||||
<div
|
||||
className={`text-xs mt-1 opacity-75 ${
|
||||
message.messageType === 'announcement'
|
||||
? 'text-red-600'
|
||||
: message.senderId === user.id
|
||||
? 'text-white'
|
||||
: 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{formatTime(message.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Message Input */}
|
||||
<form onSubmit={handleSendMessage} className="p-4 border-t border-gray-200">
|
||||
<div className="text-xs text-gray-500 mb-2">
|
||||
{messageMode === 'public' && 'Herkese mesaj gönderiyorsunuz'}
|
||||
{messageMode === 'private' &&
|
||||
selectedRecipient &&
|
||||
`${selectedRecipient.name} kişisine özel mesaj`}
|
||||
{messageMode === 'private' && !selectedRecipient && 'Önce bir kişi seçin'}
|
||||
{messageMode === 'announcement' && 'Sınıfa duyuru gönderiyorsunuz'}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder={
|
||||
messageMode === 'private' && !selectedRecipient
|
||||
? 'Önce kişi seçin...'
|
||||
: messageMode === 'announcement'
|
||||
? 'Duyuru mesajınız...'
|
||||
: 'Mesajınızı yazın...'
|
||||
}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
maxLength={500}
|
||||
disabled={messageMode === 'private' && !selectedRecipient}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={
|
||||
!newMessage.trim() || (messageMode === 'private' && !selectedRecipient)
|
||||
}
|
||||
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<FaPaperPlane size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
case 'participants': {
|
||||
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 mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Katılımcılar ({participants.length + 1})
|
||||
</h3>
|
||||
<button onClick={() => setActiveSidePanel(null)}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex space-x-1 bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setParticipantsActiveTab('participants')}
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all ${
|
||||
participantsActiveTab === 'participants'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<FaUsers className="inline mr-1" size={14} />
|
||||
Katılımcılar
|
||||
</button>
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => setParticipantsActiveTab('attendance')}
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all ${
|
||||
participantsActiveTab === 'attendance'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<FaClipboardList className="inline mr-1" size={14} />
|
||||
Katılım Raporu
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{participantsActiveTab === 'participants' && (
|
||||
<>
|
||||
{user.role === 'teacher' && participants.length > 0 && (
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (signalRServiceRef.current && user.role === 'teacher') {
|
||||
const now = new Date()
|
||||
// Remove all non-teacher participants
|
||||
const nonTeacherIds = participants
|
||||
.filter((p) => !p.isTeacher)
|
||||
.map((p) => p.id)
|
||||
for (const participantId of nonTeacherIds) {
|
||||
await signalRServiceRef.current.kickParticipant(
|
||||
classSession.id,
|
||||
participantId,
|
||||
)
|
||||
}
|
||||
setParticipants((prev) => prev.filter((p) => p.isTeacher))
|
||||
setAttendanceRecords((prev) =>
|
||||
prev.map((r) => {
|
||||
if (nonTeacherIds.includes(r.studentId) && !r.leaveTime) {
|
||||
const leaveTime = now.toISOString()
|
||||
const join = new Date(r.joinTime)
|
||||
const leave = now
|
||||
const totalDurationMinutes = Math.max(
|
||||
1,
|
||||
Math.round((leave.getTime() - join.getTime()) / 60000),
|
||||
)
|
||||
return { ...r, leaveTime, totalDurationMinutes }
|
||||
}
|
||||
return r
|
||||
}),
|
||||
)
|
||||
}
|
||||
}}
|
||||
className="w-full bg-red-600 hover:bg-red-700 text-white p-2 shadow text-sm transition-all"
|
||||
>
|
||||
Tüm Katılımcıları Çıkar
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
<div className="space-y-2">
|
||||
{/* Current User */}
|
||||
<div className="flex items-center justify-between p-2 rounded-lg bg-blue-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-semibold">
|
||||
{user.name.charAt(0)}
|
||||
</div>
|
||||
<span className="text-gray-900">{user.name} (Siz)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
{user.role === 'teacher' && (
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
|
||||
Öğretmen
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Other Participants */}
|
||||
{participants.map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="flex items-center justify-between p-2 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-gray-500 rounded-full flex items-center justify-center text-white text-sm font-semibold">
|
||||
{participant.name.charAt(0)}
|
||||
</div>
|
||||
<span
|
||||
className="text-gray-900 cursor-pointer hover:underline"
|
||||
onClick={() => {
|
||||
setMessageMode('private')
|
||||
setSelectedRecipient({ id: participant.id, name: participant.name })
|
||||
setActiveSidePanel('chat')
|
||||
}}
|
||||
>
|
||||
{participant.name}
|
||||
</span>
|
||||
|
||||
{/* 👋 Parmak kaldırma göstergesi */}
|
||||
{participant.isHandRaised && (
|
||||
<FaHandPaper
|
||||
className="text-yellow-600 ml-2"
|
||||
title="Parmak kaldırdı"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
{/* 🎓 Öğretmen için el kaldırma kontrolü */}
|
||||
{user.role === 'teacher' &&
|
||||
!participant.isTeacher &&
|
||||
participant.isHandRaised && (
|
||||
<>
|
||||
<button
|
||||
onClick={() =>
|
||||
signalRServiceRef.current?.approveHandRaise(
|
||||
classSession.id,
|
||||
participant.id,
|
||||
)
|
||||
}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded transition-colors"
|
||||
title="El kaldırmayı onayla"
|
||||
>
|
||||
<FaCheck size={12} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
signalRServiceRef.current?.dismissHandRaise(
|
||||
classSession.id,
|
||||
participant.id,
|
||||
)
|
||||
}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="El kaldırmayı reddet"
|
||||
>
|
||||
<FaTimes size={12} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Ses aç/kapat butonu */}
|
||||
{user.role === 'teacher' && !participant.isTeacher && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await handleMuteParticipant(
|
||||
participant.id,
|
||||
!participant.isAudioMuted,
|
||||
user.role === 'teacher',
|
||||
)
|
||||
}}
|
||||
className={`p-1 rounded transition-colors ${participant.isAudioMuted ? 'text-green-600 hover:bg-green-50' : 'text-yellow-600 hover:bg-yellow-50'}`}
|
||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||
>
|
||||
{participant.isAudioMuted ? (
|
||||
<FaMicrophone />
|
||||
) : (
|
||||
<FaMicrophoneSlash />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{/* Video durumu gösterimi */}
|
||||
{participant.isVideoMuted && (
|
||||
<FaVideoSlash className="text-red-500 text-sm" />
|
||||
)}
|
||||
{participant.isTeacher && (
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
|
||||
Öğretmen
|
||||
</span>
|
||||
)}
|
||||
{user.role === 'teacher' && !participant.isTeacher && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setKickingParticipant({
|
||||
id: participant.id,
|
||||
name: participant.name,
|
||||
})
|
||||
}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="Sınıftan Çıkar"
|
||||
>
|
||||
<FaUserTimes size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{participantsActiveTab === 'attendance' && (
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{attendanceRecords.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FaClipboardList size={32} className="mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-sm text-gray-600">Henüz katılım kaydı bulunmamaktadır.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{attendanceRecords.map((record) => (
|
||||
<div key={record.id} className="p-3 border border-gray-200 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-gray-800">{record.studentName}</h4>
|
||||
<span className="text-sm font-semibold text-blue-600">
|
||||
{formatDuration(record.totalDurationMinutes)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 space-y-1">
|
||||
<div>Giriş: {formatTime(record.joinTime)}</div>
|
||||
<div>
|
||||
Çıkış:{' '}
|
||||
{record.leaveTime ? formatTime(record.leaveTime) : 'Devam ediyor'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
case 'documents':
|
||||
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 Dokümanları</h3>
|
||||
<button onClick={() => setActiveSidePanel(null)}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{/* Upload Area (Teacher Only) */}
|
||||
{user.role === 'teacher' && (
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-6 mb-4 text-center transition-colors ${
|
||||
dragOver
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
}}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
>
|
||||
<FaFile size={32} className="mx-auto text-gray-400 mb-2" />
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Doküman Yükle</p>
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
Dosyaları buraya sürükleyin veya seçin
|
||||
</p>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Dosya Seç
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
accept=".pdf,.doc,.docx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.odp"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documents List */}
|
||||
{documents.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FaFile size={32} className="mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-sm">Henüz doküman yüklenmemiş.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{documents.map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:shadow-sm transition-shadow"
|
||||
>
|
||||
<div className="flex items-center space-x-3 min-w-0 flex-1">
|
||||
<div className="text-lg flex-shrink-0">{getFileIcon(doc.type)}</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-medium text-gray-800 text-sm truncate">
|
||||
{doc.name}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-600">
|
||||
{formatFileSize(doc.size)} •{' '}
|
||||
{new Date(doc.uploadedAt).toLocaleDateString('tr-TR')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{doc.uploadedBy}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => handleViewDocument(doc)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye size={12} />
|
||||
</button>
|
||||
|
||||
<a
|
||||
href={doc.url}
|
||||
download={doc.name}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded transition-colors"
|
||||
title="İndir"
|
||||
>
|
||||
<FaDownload size={12} />
|
||||
</a>
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => handleDeleteDocument(doc.id)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'layout':
|
||||
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">Video Layout Seçin</h3>
|
||||
<button onClick={() => setActiveSidePanel(null)}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
<div className="space-y-3">
|
||||
{layouts.map((layout) => (
|
||||
<button
|
||||
key={layout.id}
|
||||
onClick={() => {
|
||||
handleLayoutChange(layout)
|
||||
}}
|
||||
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
|
||||
currentLayout.id === layout.id
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<div
|
||||
className={`p-2 rounded-full ${
|
||||
currentLayout.id === layout.id
|
||||
? 'bg-blue-100 text-blue-600'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{getLayoutIcon(layout.type)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 text-sm">{layout.name}</h4>
|
||||
<p className="text-xs text-gray-600">{layout.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layout Preview */}
|
||||
<div className="bg-gray-100 rounded p-3 h-16 flex items-center justify-center">
|
||||
{layout.type === 'grid' && (
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="w-4 h-3 bg-blue-300 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{layout.type === 'sidebar' && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-6 bg-blue-500 rounded"></div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{layout.type === 'teacher-focus' && (
|
||||
<div className="space-y-1">
|
||||
<div className="w-12 h-4 bg-green-500 rounded"></div>
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
<div className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'settings':
|
||||
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={() => setActiveSidePanel(null)}>
|
||||
<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) =>
|
||||
handleSettingsChange({ 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) =>
|
||||
handleSettingsChange({ 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) =>
|
||||
handleSettingsChange({ 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) =>
|
||||
handleSettingsChange({ 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) =>
|
||||
handleSettingsChange({
|
||||
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) =>
|
||||
handleSettingsChange({
|
||||
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) => handleSettingsChange({ 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) =>
|
||||
handleSettingsChange({ 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>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full lg:w-80 bg-white shadow-xl border-l border-gray-200 flex-shrink-0 h-full">
|
||||
{panelContent()}
|
||||
{/* Kapat butonu kaldırıldı, ana panelde gösterilecek */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in a new issue