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"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.48gll4p3s3o"
|
"revision": "0.d148klvmpj8"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
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 { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
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 =
|
type SidePanelType =
|
||||||
| 'chat'
|
| 'chat'
|
||||||
|
|
@ -659,824 +664,78 @@ const RoomDetail: React.FC = () => {
|
||||||
const renderSidePanel = () => {
|
const renderSidePanel = () => {
|
||||||
if (!activeSidePanel) return null
|
if (!activeSidePanel) return null
|
||||||
|
|
||||||
const panelContent = () => {
|
|
||||||
switch (activeSidePanel) {
|
switch (activeSidePanel) {
|
||||||
case 'chat': {
|
case 'chat':
|
||||||
const availableRecipients = participants.filter((p) => p.id !== user.id)
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
<ChatPanel
|
||||||
<div className="p-3 sm:p-4 border-b border-gray-200">
|
user={user}
|
||||||
<div className="flex items-center justify-between">
|
participants={participants}
|
||||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900">
|
chatMessages={chatMessages}
|
||||||
Sınıf Sohbeti
|
newMessage={newMessage}
|
||||||
</h3>
|
setNewMessage={setNewMessage}
|
||||||
<button
|
messageMode={messageMode}
|
||||||
onClick={() => setActiveSidePanel(null)}
|
setMessageMode={setMessageMode}
|
||||||
className="p-1 hover:bg-gray-100 rounded lg:hidden"
|
selectedRecipient={selectedRecipient}
|
||||||
>
|
setSelectedRecipient={setSelectedRecipient}
|
||||||
<FaTimes className="text-gray-500" size={16} />
|
onSendMessage={handleSendMessage}
|
||||||
</button>
|
onClose={() => setActiveSidePanel(null)}
|
||||||
</div>
|
formatTime={formatTime}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
<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((participant) => (
|
|
||||||
<option key={participant.id} value={participant.id}>
|
|
||||||
{participant.name} {participant.isTeacher ? '(Öğretmen)' : ''}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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': {
|
case 'participants':
|
||||||
return (
|
return (
|
||||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
<ParticipantsPanel
|
||||||
<div className="p-4 border-b border-gray-200">
|
user={user}
|
||||||
<div className="flex items-center justify-between mb-3">
|
participants={participants}
|
||||||
<h3 className="text-lg font-semibold text-gray-900">
|
attendanceRecords={attendanceRecords}
|
||||||
Katılımcılar ({participants.length + 1})
|
onMuteParticipant={handleMuteParticipant}
|
||||||
</h3>
|
onKickParticipant={handleKickParticipant}
|
||||||
<button onClick={() => setActiveSidePanel(null)}>
|
onClose={() => setActiveSidePanel(null)}
|
||||||
<FaTimes className="text-gray-500" />
|
formatTime={formatTime}
|
||||||
</button>
|
formatDuration={formatDuration}
|
||||||
</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':
|
case 'documents':
|
||||||
return (
|
return (
|
||||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
<DocumentsPanel
|
||||||
<div className="p-4 border-b border-gray-200">
|
user={user}
|
||||||
<div className="flex items-center justify-between">
|
documents={documents}
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Sınıf Dokümanları</h3>
|
onUpload={handleUploadDocument}
|
||||||
<button onClick={() => setActiveSidePanel(null)}>
|
onDelete={handleDeleteDocument}
|
||||||
<FaTimes className="text-gray-500" />
|
onView={handleViewDocument}
|
||||||
</button>
|
onClose={() => setActiveSidePanel(null)}
|
||||||
</div>
|
formatFileSize={formatFileSize}
|
||||||
</div>
|
getFileIcon={getFileIcon}
|
||||||
|
|
||||||
<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':
|
case 'layout':
|
||||||
return (
|
return (
|
||||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
<LayoutPanel
|
||||||
<div className="p-4 border-b border-gray-200">
|
layouts={layouts}
|
||||||
<div className="flex items-center justify-between">
|
currentLayout={currentLayout}
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Video Layout Seçin</h3>
|
onChangeLayout={handleLayoutChange}
|
||||||
<button onClick={() => setActiveSidePanel(null)}>
|
onClose={() => 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':
|
case 'settings':
|
||||||
return (
|
return (
|
||||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
<SettingsPanel
|
||||||
<div className="p-4 border-b border-gray-200">
|
user={user}
|
||||||
<div className="flex items-center justify-between">
|
classSettings={classSettings}
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Sınıf Ayarları</h3>
|
onSettingsChange={handleSettingsChange}
|
||||||
<button onClick={() => setActiveSidePanel(null)}>
|
onClose={() => 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:
|
default:
|
||||||
return null
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet
|
<Helmet
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue