Parmak kaldırma özelliği

This commit is contained in:
Sedat ÖZTÜRK 2025-08-29 15:04:22 +03:00
parent f3b953da4f
commit df415dd04d
8 changed files with 52 additions and 844 deletions

View file

@ -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);
} }

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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')}