Parmak kaldırma özelliği
This commit is contained in:
parent
f3b953da4f
commit
df415dd04d
8 changed files with 52 additions and 844 deletions
|
|
@ -270,6 +270,17 @@ public class ClassroomHub : Hub
|
||||||
[HubMethodName("RaiseHand")]
|
[HubMethodName("RaiseHand")]
|
||||||
public async Task RaiseHandAsync(Guid sessionId, Guid studentId, string studentName)
|
public async Task RaiseHandAsync(Guid sessionId, Guid studentId, string studentName)
|
||||||
{
|
{
|
||||||
|
// 🔑 Participant'ı bul
|
||||||
|
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||||
|
x => x.SessionId == sessionId && x.UserId == studentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (participant != null)
|
||||||
|
{
|
||||||
|
participant.IsHandRaised = true;
|
||||||
|
await _participantRepository.UpdateAsync(participant, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseReceived", new
|
await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseReceived", new
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
|
|
@ -316,14 +327,36 @@ public class ClassroomHub : Hub
|
||||||
}
|
}
|
||||||
|
|
||||||
[HubMethodName("ApproveHandRaise")]
|
[HubMethodName("ApproveHandRaise")]
|
||||||
public async Task ApproveHandRaiseAsync(Guid sessionId, Guid handRaiseId)
|
public async Task ApproveHandRaiseAsync(Guid sessionId, Guid handRaiseId, Guid studentId)
|
||||||
{
|
{
|
||||||
|
// 🔑 Öğrencinin parmak kaldırma durumunu sıfırla
|
||||||
|
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||||
|
x => x.SessionId == sessionId && x.UserId == studentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (participant != null)
|
||||||
|
{
|
||||||
|
participant.IsHandRaised = false;
|
||||||
|
await _participantRepository.UpdateAsync(participant, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", handRaiseId);
|
await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", handRaiseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HubMethodName("DismissHandRaise")]
|
[HubMethodName("DismissHandRaise")]
|
||||||
public async Task DismissHandRaiseAsync(Guid sessionId, Guid handRaiseId)
|
public async Task DismissHandRaiseAsync(Guid sessionId, Guid handRaiseId, Guid studentId)
|
||||||
{
|
{
|
||||||
|
// 🔑 Participant'ı bul ve elini indir
|
||||||
|
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||||
|
x => x.SessionId == sessionId && x.UserId == studentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (participant != null)
|
||||||
|
{
|
||||||
|
participant.IsHandRaised = false;
|
||||||
|
await _participantRepository.UpdateAsync(participant, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", handRaiseId);
|
await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", handRaiseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
import { ClassroomAttendanceDto } from '@/proxy/classroom/models'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { FaClock, FaUsers } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface AttendancePanelProps {
|
|
||||||
attendanceRecords: ClassroomAttendanceDto[]
|
|
||||||
isOpen: boolean
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AttendancePanel: React.FC<AttendancePanelProps> = ({
|
|
||||||
attendanceRecords,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
// Anlık süre güncellemesi için state ve timer
|
|
||||||
const [now, setNow] = useState(Date.now())
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return
|
|
||||||
const interval = setInterval(() => setNow(Date.now()), 60000) // her dakika
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
const formatDuration = (minutes: number) => {
|
|
||||||
const hours = Math.floor(minutes / 60)
|
|
||||||
const mins = minutes % 60
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${mins}m`
|
|
||||||
}
|
|
||||||
return `${mins}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTime = (timeString: string) => {
|
|
||||||
return new Date(timeString).toLocaleTimeString('tr-TR', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden">
|
|
||||||
<div className="p-6 border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaUsers className="text-blue-600" size={24} />
|
|
||||||
<h2 className="text-2xl font-bold text-gray-800">Katılım Raporu</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="text-gray-500 hover:text-gray-700 text-xl font-bold"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 overflow-y-auto max-h-96">
|
|
||||||
{attendanceRecords.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
<FaClock size={48} className="mx-auto mb-4" />
|
|
||||||
<p>Henüz katılım kaydı bulunmamaktadır.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full border-collapse border border-gray-300">
|
|
||||||
<thead>
|
|
||||||
<tr className="bg-gray-50">
|
|
||||||
<th className="border border-gray-300 px-4 py-3 text-left font-semibold text-gray-700">
|
|
||||||
Öğrenci Adı
|
|
||||||
</th>
|
|
||||||
<th className="border border-gray-300 px-4 py-3 text-left font-semibold text-gray-700">
|
|
||||||
Giriş Saati
|
|
||||||
</th>
|
|
||||||
<th className="border border-gray-300 px-4 py-3 text-left font-semibold text-gray-700">
|
|
||||||
Çıkış Saati
|
|
||||||
</th>
|
|
||||||
<th className="border border-gray-300 px-4 py-3 text-left font-semibold text-gray-700">
|
|
||||||
Toplam Süre
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{attendanceRecords.map((record) => (
|
|
||||||
<tr key={record.id} className="hover:bg-gray-50">
|
|
||||||
<td className="border border-gray-300 px-4 py-3 text-gray-800">
|
|
||||||
{record.studentName}
|
|
||||||
</td>
|
|
||||||
<td className="border border-gray-300 px-4 py-3 text-gray-600">
|
|
||||||
{formatTime(record.joinTime)}
|
|
||||||
</td>
|
|
||||||
<td className="border border-gray-300 px-4 py-3 text-gray-600">
|
|
||||||
{record.leaveTime ? formatTime(record.leaveTime) : 'Devam ediyor'}
|
|
||||||
</td>
|
|
||||||
<td className="border border-gray-300 px-4 py-3 text-gray-800 font-semibold">
|
|
||||||
{(() => {
|
|
||||||
// Her zaman canlı süreyi hesapla, çıkış varsa oraya kadar, yoksa şimdiye kadar
|
|
||||||
const endTime = record.leaveTime
|
|
||||||
? new Date(record.leaveTime).getTime()
|
|
||||||
: now
|
|
||||||
const join = new Date(record.joinTime).getTime()
|
|
||||||
const mins = Math.floor((endTime - join) / 60000)
|
|
||||||
return formatDuration(mins)
|
|
||||||
})()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
import { ClassroomChatDto } from '@/proxy/classroom/models'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
import React, { useState, useRef, useEffect } from 'react'
|
|
||||||
import { FaPaperPlane, FaComments, FaTimes, FaUsers, FaUser, FaBullhorn } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface ChatPanelProps {
|
|
||||||
messages: ClassroomChatDto[]
|
|
||||||
isTeacher: boolean
|
|
||||||
isOpen: boolean
|
|
||||||
onClose: () => void
|
|
||||||
onSendMessage: (message: string) => void
|
|
||||||
participants: Array<{ id: string; name: string; isTeacher: boolean }>
|
|
||||||
onSendPrivateMessage: (message: string, recipientId: string, recipientName: string) => void
|
|
||||||
onSendAnnouncement?: (message: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatPanel: React.FC<ChatPanelProps> = ({
|
|
||||||
messages,
|
|
||||||
isTeacher,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onSendMessage,
|
|
||||||
participants,
|
|
||||||
onSendPrivateMessage,
|
|
||||||
onSendAnnouncement,
|
|
||||||
}) => {
|
|
||||||
const { user } = useStoreState((state) => state.auth)
|
|
||||||
|
|
||||||
const [newMessage, setNewMessage] = useState('')
|
|
||||||
const [messageMode, setMessageMode] = useState<'public' | 'private' | 'announcement'>('public')
|
|
||||||
const [selectedRecipient, setSelectedRecipient] = useState<{ id: string; name: string } | null>(
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
scrollToBottom()
|
|
||||||
}, [messages])
|
|
||||||
|
|
||||||
const handleSendMessage = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (newMessage.trim()) {
|
|
||||||
if (messageMode === 'private' && selectedRecipient) {
|
|
||||||
onSendPrivateMessage(newMessage.trim(), selectedRecipient.id, selectedRecipient.name)
|
|
||||||
} else if (messageMode === 'announcement' && onSendAnnouncement) {
|
|
||||||
onSendAnnouncement(newMessage.trim())
|
|
||||||
} else {
|
|
||||||
onSendMessage(newMessage.trim())
|
|
||||||
}
|
|
||||||
setNewMessage('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTime = (timestamp: string) => {
|
|
||||||
return new Date(timestamp).toLocaleTimeString('tr-TR', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
const availableRecipients = participants.filter((p) => p.id !== user.id)
|
|
||||||
return (
|
|
||||||
<div className="fixed right-4 bottom-4 w-96 h-[500px] bg-white rounded-lg shadow-xl border border-gray-200 flex flex-col z-50">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-blue-50 rounded-t-lg">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaComments className="text-blue-600" size={20} />
|
|
||||||
<h3 className="font-semibold text-gray-800">Sınıf Sohbeti</h3>
|
|
||||||
</div>
|
|
||||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 transition-colors">
|
|
||||||
<FaTimes size={18} />
|
|
||||||
</button>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{isTeacher && (
|
|
||||||
<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">
|
|
||||||
{messages.length === 0 ? (
|
|
||||||
<div className="text-center text-gray-500 text-sm">Henüz mesaj bulunmamaktadır.</div>
|
|
||||||
) : (
|
|
||||||
messages.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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
import { VideoLayoutDto } from '@/proxy/classroom/models'
|
|
||||||
import React from 'react'
|
|
||||||
import { FaExpand, FaTh, FaColumns, FaDesktop, FaChalkboardTeacher } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface ClassLayoutPanelProps {
|
|
||||||
currentLayout: VideoLayoutDto
|
|
||||||
isOpen: boolean
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const layouts: VideoLayoutDto[] = [
|
|
||||||
{
|
|
||||||
id: 'grid',
|
|
||||||
name: 'Izgara Görünümü',
|
|
||||||
type: 'grid',
|
|
||||||
description: 'Tüm katılımcılar eşit boyutta görünür',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sidebar',
|
|
||||||
name: 'Yan Panel Görünümü',
|
|
||||||
type: 'sidebar',
|
|
||||||
description: 'Ana konuşmacı büyük, diğerleri yan panelde',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'teacher-focus',
|
|
||||||
name: 'Öğretmen Odaklı',
|
|
||||||
type: 'teacher-focus',
|
|
||||||
description: 'Öğretmen tam ekranda görünür, öğrenciler küçük panelde',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const ClassLayoutPanel: React.FC<ClassLayoutPanelProps> = ({
|
|
||||||
currentLayout,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
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 <FaColumns size={24} />
|
|
||||||
case 'teacher-focus':
|
|
||||||
// Sade, tek kişilik bir ikon (öğretmen)
|
|
||||||
return <FaChalkboardTeacher size={26} />
|
|
||||||
default:
|
|
||||||
return <FaTh size={24} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="p-6 border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900">Video Layout Seçin</h2>
|
|
||||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 text-xl font-bold">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{layouts.map((layout) => (
|
|
||||||
<div key={layout.id}>
|
|
||||||
<div className="flex items-center space-x-4 mb-3">
|
|
||||||
<div
|
|
||||||
className={`p-3 rounded-full ${
|
|
||||||
currentLayout.id === layout.id
|
|
||||||
? 'bg-blue-100 text-blue-600'
|
|
||||||
: 'bg-gray-100 text-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getLayoutIcon(layout.type)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-gray-900">{layout.name}</h3>
|
|
||||||
<p className="text-sm text-gray-600">{layout.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-100 rounded-lg p-4 h-24 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-6 h-4 bg-blue-300 rounded"></div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{layout.type === 'sidebar' && (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="w-12 h-8 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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,244 +0,0 @@
|
||||||
import React, { useState, useRef } from 'react'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import {
|
|
||||||
FaFile,
|
|
||||||
FaUpload,
|
|
||||||
FaDownload,
|
|
||||||
FaTrash,
|
|
||||||
FaEye,
|
|
||||||
FaTimes,
|
|
||||||
FaFilePdf,
|
|
||||||
FaFileWord,
|
|
||||||
FaFileImage,
|
|
||||||
FaFileAlt,
|
|
||||||
FaPlay,
|
|
||||||
FaStop,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import { ClassDocumentDto } from '@/proxy/classroom/models'
|
|
||||||
|
|
||||||
interface DocumentPanelProps {
|
|
||||||
documents: ClassDocumentDto[]
|
|
||||||
isOpen: boolean
|
|
||||||
onClose: () => void
|
|
||||||
onUpload?: (file: File) => void
|
|
||||||
onDelete?: (documentId: string) => void
|
|
||||||
onView?: (document: ClassDocumentDto) => void
|
|
||||||
isTeacher: boolean
|
|
||||||
onStartPresentation?: (document: ClassDocumentDto) => void
|
|
||||||
onStopPresentation?: () => void
|
|
||||||
activePresentationId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DocumentPanel: React.FC<DocumentPanelProps> = ({
|
|
||||||
documents,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onUpload,
|
|
||||||
onDelete,
|
|
||||||
onView,
|
|
||||||
isTeacher,
|
|
||||||
onStartPresentation,
|
|
||||||
onStopPresentation,
|
|
||||||
activePresentationId,
|
|
||||||
}) => {
|
|
||||||
const [dragOver, setDragOver] = useState(false)
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const getFileIcon = (type: string) => {
|
|
||||||
if (type.includes('pdf')) return <FaFilePdf className="text-red-500" />
|
|
||||||
if (
|
|
||||||
type.includes('word') ||
|
|
||||||
type.includes('doc') ||
|
|
||||||
type.includes('presentation') ||
|
|
||||||
type.includes('powerpoint')
|
|
||||||
)
|
|
||||||
return <FaFileWord className="text-blue-500" />
|
|
||||||
if (type.includes('image')) return <FaFileImage className="text-green-500" />
|
|
||||||
return <FaFileAlt className="text-gray-500" />
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatFileSize = (bytes: number) => {
|
|
||||||
if (bytes === 0) return '0 Bytes'
|
|
||||||
const k = 1024
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPresentationFile = (type: string, name: string) => {
|
|
||||||
return (
|
|
||||||
type.includes('presentation') ||
|
|
||||||
type.includes('powerpoint') ||
|
|
||||||
name.toLowerCase().includes('.ppt') ||
|
|
||||||
name.toLowerCase().includes('.pptx') ||
|
|
||||||
type.includes('pdf')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setDragOver(false)
|
|
||||||
|
|
||||||
if (!isTeacher || !onUpload) return
|
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files)
|
|
||||||
files.forEach((file) => onUpload(file))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (!isTeacher || !onUpload) return
|
|
||||||
|
|
||||||
const files = Array.from(e.target.files || [])
|
|
||||||
files.forEach((file) => onUpload(file))
|
|
||||||
|
|
||||||
// Reset input
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
className="bg-white rounded-lg max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaFile className="text-blue-600" size={24} />
|
|
||||||
<h2 className="text-2xl font-bold text-gray-800">Sınıf Dokümanları</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="text-gray-500 hover:text-gray-700 text-xl font-bold"
|
|
||||||
>
|
|
||||||
<FaTimes size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 overflow-y-auto max-h-96">
|
|
||||||
{/* Upload Area (Teacher Only) */}
|
|
||||||
{isTeacher && (
|
|
||||||
<div
|
|
||||||
className={`border-2 border-dashed rounded-lg p-8 mb-6 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)}
|
|
||||||
>
|
|
||||||
<FaUpload size={48} className="mx-auto text-gray-400 mb-4" />
|
|
||||||
<p className="text-lg font-medium text-gray-700 mb-2">Doküman Yükle</p>
|
|
||||||
<p className="text-gray-500 mb-4">Dosyaları buraya sürükleyin veya seçin</p>
|
|
||||||
<button
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg 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={48} className="mx-auto mb-4 text-gray-300" />
|
|
||||||
<p>Henüz doküman yüklenmemiş.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
{documents.map((doc) => (
|
|
||||||
<div
|
|
||||||
key={doc.id}
|
|
||||||
className="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="text-2xl">{getFileIcon(doc.type)}</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-gray-800">{doc.name}</h3>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{formatFileSize(doc.size)} •{' '}
|
|
||||||
{new Date(doc.uploadedAt).toLocaleDateString('tr-TR')}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">Yükleyen: {doc.uploadedBy}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => onView?.(doc)}
|
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
|
||||||
title="Görüntüle"
|
|
||||||
>
|
|
||||||
<FaEye size={16} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Sunum Başlat/Durdur Butonu */}
|
|
||||||
{isTeacher && isPresentationFile(doc.type, doc.name) && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (activePresentationId === doc.id) {
|
|
||||||
onStopPresentation?.()
|
|
||||||
} else {
|
|
||||||
onStartPresentation?.(doc)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`p-2 rounded-lg transition-colors ${
|
|
||||||
activePresentationId === doc.id
|
|
||||||
? 'text-red-600 hover:bg-red-50'
|
|
||||||
: 'text-green-600 hover:bg-green-50'
|
|
||||||
}`}
|
|
||||||
title={activePresentationId === doc.id ? 'Sunumu Durdur' : 'Sunum Başlat'}
|
|
||||||
>
|
|
||||||
{activePresentationId === doc.id ? (
|
|
||||||
<FaStop size={16} />
|
|
||||||
) : (
|
|
||||||
<FaPlay size={16} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<a
|
|
||||||
href={doc.url}
|
|
||||||
download={doc.name}
|
|
||||||
className="p-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
|
||||||
title="İndir"
|
|
||||||
>
|
|
||||||
<FaDownload size={16} />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{isTeacher && (
|
|
||||||
<button
|
|
||||||
onClick={() => onDelete?.(doc.id)}
|
|
||||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
title="Sil"
|
|
||||||
>
|
|
||||||
<FaTrash size={16} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import { HandRaiseDto } from '@/proxy/classroom/models'
|
|
||||||
import React from 'react'
|
|
||||||
import { FaHandPaper, FaTimes, FaCheck } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface HandRaisePanelProps {
|
|
||||||
handRaises: HandRaiseDto[]
|
|
||||||
isOpen: boolean
|
|
||||||
onClose: () => void
|
|
||||||
onApprove?: (handRaiseId: string) => void
|
|
||||||
onDismiss?: (handRaiseId: string) => void
|
|
||||||
isTeacher: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HandRaisePanel: React.FC<HandRaisePanelProps> = ({
|
|
||||||
handRaises,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onApprove,
|
|
||||||
onDismiss,
|
|
||||||
isTeacher,
|
|
||||||
}) => {
|
|
||||||
const formatTime = (timestamp: string) => {
|
|
||||||
return new Date(timestamp).toLocaleTimeString('tr-TR', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTimeSince = (timestamp: string) => {
|
|
||||||
const now = new Date()
|
|
||||||
const time = new Date(timestamp)
|
|
||||||
const diffMinutes = Math.floor((now.getTime() - time.getTime()) / 60000)
|
|
||||||
|
|
||||||
if (diffMinutes < 1) return 'Az önce'
|
|
||||||
if (diffMinutes < 60) return `${diffMinutes} dakika önce`
|
|
||||||
const hours = Math.floor(diffMinutes / 60)
|
|
||||||
return `${hours} saat önce`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
const activeHandRaises = handRaises.filter((hr) => hr.isActive)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg max-w-2xl w-full mx-4 max-h-[80vh] overflow-hidden">
|
|
||||||
<div className="p-6 border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaHandPaper className="text-yellow-600" size={24} />
|
|
||||||
<h2 className="text-2xl font-bold text-gray-800">
|
|
||||||
Parmak Kaldıranlar ({activeHandRaises.length})
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="text-gray-500 hover:text-gray-700 text-xl font-bold"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 overflow-y-auto max-h-96">
|
|
||||||
{activeHandRaises.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
<FaHandPaper size={48} className="mx-auto mb-4 text-gray-300" />
|
|
||||||
<p>Şu anda parmak kaldıran öğrenci bulunmamaktadır.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{activeHandRaises.map((handRaise) => (
|
|
||||||
<div
|
|
||||||
key={handRaise.id}
|
|
||||||
className="flex items-center justify-between p-4 bg-yellow-50 border border-yellow-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<FaHandPaper className="text-yellow-600" size={20} />
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-gray-800">{handRaise.studentName}</h3>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{formatTime(handRaise.timestamp)} • {getTimeSince(handRaise.timestamp)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isTeacher && (
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => onApprove?.(handRaise.id)}
|
|
||||||
className="flex items-center space-x-1 px-3 py-1 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
<FaCheck size={14} />
|
|
||||||
<span>Onayla</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => onDismiss?.(handRaise.id)}
|
|
||||||
className="flex items-center space-x-1 px-3 py-1 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
<FaTimes size={14} />
|
|
||||||
<span>Reddet</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -50,7 +50,7 @@ import {
|
||||||
} from '@/proxy/classroom/models'
|
} from '@/proxy/classroom/models'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import { ParticipantGrid } from '@/components/classroom/ParticipantGrid'
|
import { ParticipantGrid } from '@/components/classroom/ParticipantGrid'
|
||||||
import { ScreenSharePanel } from '@/components/classroom/Panels/ScreenSharePanel'
|
import { ScreenSharePanel } from '@/components/classroom/ScreenSharePanel'
|
||||||
import { KickParticipantModal } from '@/components/classroom/KickParticipantModal'
|
import { KickParticipantModal } from '@/components/classroom/KickParticipantModal'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
|
|
@ -1939,6 +1939,22 @@ const RoomDetail: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Parmak Kaldır (Öğrenci) */}
|
||||||
|
{user.role === 'student' && classSettings.allowHandRaise && (
|
||||||
|
<button
|
||||||
|
onClick={handleRaiseHand}
|
||||||
|
disabled={hasRaisedHand}
|
||||||
|
className={`p-2 rounded-lg transition-all ${
|
||||||
|
hasRaisedHand
|
||||||
|
? 'bg-yellow-600 text-white cursor-not-allowed'
|
||||||
|
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
|
||||||
|
}`}
|
||||||
|
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
||||||
|
>
|
||||||
|
<FaHandPaper size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Participants */}
|
{/* Participants */}
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleSidePanel('participants')}
|
onClick={() => toggleSidePanel('participants')}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue