diff --git a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs index fc44f474..b2205f35 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs @@ -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); } diff --git a/ui/src/components/classroom/Panels/AttendancePanel.tsx b/ui/src/components/classroom/Panels/AttendancePanel.tsx deleted file mode 100644 index 6c18da72..00000000 --- a/ui/src/components/classroom/Panels/AttendancePanel.tsx +++ /dev/null @@ -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 = ({ - 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 ( -
-
-
-
-
- -

Katılım Raporu

-
- -
-
- -
- {attendanceRecords.length === 0 ? ( -
- -

Henüz katılım kaydı bulunmamaktadır.

-
- ) : ( -
- - - - - - - - - - - {attendanceRecords.map((record) => ( - - - - - - - ))} - -
- Öğrenci Adı - - Giriş Saati - - Çıkış Saati - - Toplam Süre -
- {record.studentName} - - {formatTime(record.joinTime)} - - {record.leaveTime ? formatTime(record.leaveTime) : 'Devam ediyor'} - - {(() => { - // 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) - })()} -
-
- )} -
-
-
- ) -} diff --git a/ui/src/components/classroom/Panels/ChatPanel.tsx b/ui/src/components/classroom/Panels/ChatPanel.tsx deleted file mode 100644 index 7926dfde..00000000 --- a/ui/src/components/classroom/Panels/ChatPanel.tsx +++ /dev/null @@ -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 = ({ - 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(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 ( -
- {/* Header */} -
-
- -

Sınıf Sohbeti

-
- -
- - {/* Message Mode Selector */} -
-
- - - - - {isTeacher && ( - - )} -
- - {messageMode === 'private' && ( - - )} -
- - {/* Messages */} -
- {messages.length === 0 ? ( -
Henüz mesaj bulunmamaktadır.
- ) : ( - messages.map((message) => ( -
-
- {message.senderId !== user.id && ( -
- {message.senderName} - {message.isTeacher && ' (Öğretmen)'} - {message.messageType === 'private' && - message.recipientId === user.id && - ' (Size özel)'} -
- )} - {message.messageType === 'private' && message.senderId === user.id && ( -
→ {message.recipientName}
- )} - {message.messageType === 'announcement' && ( -
📢 DUYURU - {message.senderName}
- )} -
{message.message}
-
- {formatTime(message.timestamp)} -
-
-
- )) - )} -
-
- - {/* Message Input */} -
-
- {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'} -
-
- 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} - /> - -
-
-
- ) -} diff --git a/ui/src/components/classroom/Panels/ClassLayoutPanel.tsx b/ui/src/components/classroom/Panels/ClassLayoutPanel.tsx deleted file mode 100644 index 40aac35c..00000000 --- a/ui/src/components/classroom/Panels/ClassLayoutPanel.tsx +++ /dev/null @@ -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 = ({ - currentLayout, - isOpen, - onClose, -}) => { - const getLayoutIcon = (type: string) => { - switch (type) { - case 'grid': - return - case 'speaker': - return - case 'presentation': - return - case 'sidebar': - return - case 'teacher-focus': - // Sade, tek kişilik bir ikon (öğretmen) - return - default: - return - } - } - - if (!isOpen) return null - - return ( -
-
-
-

Video Layout Seçin

- -
-
- -
-
- {layouts.map((layout) => ( -
-
-
- {getLayoutIcon(layout.type)} -
-
-

{layout.name}

-

{layout.description}

-
-
- -
- {layout.type === 'grid' && ( -
- {[1, 2, 3, 4].map((i) => ( -
- ))} -
- )} - {layout.type === 'sidebar' && ( -
-
-
-
-
-
-
-
-
-
-
- )} -
-
- ))} -
-
-
- ) -} diff --git a/ui/src/components/classroom/Panels/DocumentPanel.tsx b/ui/src/components/classroom/Panels/DocumentPanel.tsx deleted file mode 100644 index 4fbfd581..00000000 --- a/ui/src/components/classroom/Panels/DocumentPanel.tsx +++ /dev/null @@ -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 = ({ - documents, - isOpen, - onClose, - onUpload, - onDelete, - onView, - isTeacher, - onStartPresentation, - onStopPresentation, - activePresentationId, -}) => { - const [dragOver, setDragOver] = useState(false) - const fileInputRef = useRef(null) - - const getFileIcon = (type: string) => { - if (type.includes('pdf')) return - if ( - type.includes('word') || - type.includes('doc') || - type.includes('presentation') || - type.includes('powerpoint') - ) - return - if (type.includes('image')) return - return - } - - 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) => { - 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 ( -
- -
-
-
- -

Sınıf Dokümanları

-
- -
-
- -
- {/* Upload Area (Teacher Only) */} - {isTeacher && ( -
{ - e.preventDefault() - setDragOver(true) - }} - onDragLeave={() => setDragOver(false)} - > - -

Doküman Yükle

-

Dosyaları buraya sürükleyin veya seçin

- - -
- )} - - {/* Documents List */} - {documents.length === 0 ? ( -
- -

Henüz doküman yüklenmemiş.

-
- ) : ( -
- {documents.map((doc) => ( -
-
-
{getFileIcon(doc.type)}
-
-

{doc.name}

-

- {formatFileSize(doc.size)} •{' '} - {new Date(doc.uploadedAt).toLocaleDateString('tr-TR')} -

-

Yükleyen: {doc.uploadedBy}

-
-
- -
- - - {/* Sunum Başlat/Durdur Butonu */} - {isTeacher && isPresentationFile(doc.type, doc.name) && ( - - )} - - - - - - {isTeacher && ( - - )} -
-
- ))} -
- )} -
-
-
- ) -} diff --git a/ui/src/components/classroom/Panels/HandRaisePanel.tsx b/ui/src/components/classroom/Panels/HandRaisePanel.tsx deleted file mode 100644 index 79569487..00000000 --- a/ui/src/components/classroom/Panels/HandRaisePanel.tsx +++ /dev/null @@ -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 = ({ - 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 ( -
-
-
-
-
- -

- Parmak Kaldıranlar ({activeHandRaises.length}) -

-
- -
-
- -
- {activeHandRaises.length === 0 ? ( -
- -

Şu anda parmak kaldıran öğrenci bulunmamaktadır.

-
- ) : ( -
- {activeHandRaises.map((handRaise) => ( -
-
- -
-

{handRaise.studentName}

-

- {formatTime(handRaise.timestamp)} • {getTimeSince(handRaise.timestamp)} -

-
-
- - {isTeacher && ( -
- - -
- )} -
- ))} -
- )} -
-
-
- ) -} diff --git a/ui/src/components/classroom/Panels/ScreenSharePanel.tsx b/ui/src/components/classroom/ScreenSharePanel.tsx similarity index 100% rename from ui/src/components/classroom/Panels/ScreenSharePanel.tsx rename to ui/src/components/classroom/ScreenSharePanel.tsx diff --git a/ui/src/views/classroom/RoomDetail.tsx b/ui/src/views/classroom/RoomDetail.tsx index 5c08eb94..431bd04e 100644 --- a/ui/src/views/classroom/RoomDetail.tsx +++ b/ui/src/views/classroom/RoomDetail.tsx @@ -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 = () => { )} + {/* Parmak Kaldır (Öğrenci) */} + {user.role === 'student' && classSettings.allowHandRaise && ( + + )} + {/* Participants */}