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")]
|
||||
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
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
|
|
@ -316,14 +327,36 @@ public class ClassroomHub : Hub
|
|||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
import { useStoreState } from '@/store/store'
|
||||
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 { useParams } from 'react-router-dom'
|
||||
import {
|
||||
|
|
@ -1939,6 +1939,22 @@ const RoomDetail: React.FC = () => {
|
|||
</button>
|
||||
)}
|
||||
|
||||
{/* Parmak Kaldır (Öğrenci) */}
|
||||
{user.role === 'student' && classSettings.allowHandRaise && (
|
||||
<button
|
||||
onClick={handleRaiseHand}
|
||||
disabled={hasRaisedHand}
|
||||
className={`p-2 rounded-lg transition-all ${
|
||||
hasRaisedHand
|
||||
? 'bg-yellow-600 text-white cursor-not-allowed'
|
||||
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
|
||||
}`}
|
||||
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
||||
>
|
||||
<FaHandPaper size={14} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Participants */}
|
||||
<button
|
||||
onClick={() => toggleSidePanel('participants')}
|
||||
|
|
|
|||
Loading…
Reference in a new issue