From 18b65e6c8a1f61eb542f3f187f98db7838310fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:20:51 +0300 Subject: [PATCH] =?UTF-8?q?RoomDetail=20komponentlere=20ayr=C4=B1ld=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/dev-dist/sw.js | 2 +- ui/src/components/classroom/ChatPanel.tsx | 188 ++++ .../components/classroom/DocumentsPanel.tsx | 153 +++ ui/src/components/classroom/LayoutPanel.tsx | 112 +++ .../classroom/ParticipantsPanel.tsx | 218 +++++ ui/src/components/classroom/SettingsPanel.tsx | 164 ++++ ui/src/views/classroom/RoomDetail.tsx | 879 ++---------------- 7 files changed, 905 insertions(+), 811 deletions(-) create mode 100644 ui/src/components/classroom/ChatPanel.tsx create mode 100644 ui/src/components/classroom/DocumentsPanel.tsx create mode 100644 ui/src/components/classroom/LayoutPanel.tsx create mode 100644 ui/src/components/classroom/ParticipantsPanel.tsx create mode 100644 ui/src/components/classroom/SettingsPanel.tsx diff --git a/ui/dev-dist/sw.js b/ui/dev-dist/sw.js index 22f08234..e2d0c7ec 100644 --- a/ui/dev-dist/sw.js +++ b/ui/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.48gll4p3s3o" + "revision": "0.d148klvmpj8" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/ui/src/components/classroom/ChatPanel.tsx b/ui/src/components/classroom/ChatPanel.tsx new file mode 100644 index 00000000..c6bba61d --- /dev/null +++ b/ui/src/components/classroom/ChatPanel.tsx @@ -0,0 +1,188 @@ +import React, { useRef, useEffect } from 'react' +import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa' +import { ClassroomChatDto, ClassroomParticipantDto } from '@/proxy/classroom/models' + +interface ChatPanelProps { + user: { id: string; name: string; role: string } + participants: ClassroomParticipantDto[] + chatMessages: ClassroomChatDto[] + newMessage: string + setNewMessage: (msg: string) => void + messageMode: 'public' | 'private' | 'announcement' + setMessageMode: (mode: 'public' | 'private' | 'announcement') => void + selectedRecipient: { id: string; name: string } | null + setSelectedRecipient: (recipient: { id: string; name: string } | null) => void + onSendMessage: (e: React.FormEvent) => void + onClose: () => void + formatTime: (timestamp: string) => string +} + +const ChatPanel: React.FC = ({ + user, + participants, + chatMessages, + newMessage, + setNewMessage, + messageMode, + setMessageMode, + selectedRecipient, + setSelectedRecipient, + onSendMessage, + onClose, + formatTime, +}) => { + const messagesEndRef = useRef(null) + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [chatMessages]) + + const availableRecipients = participants.filter((p) => p.id !== user.id) + + return ( +
+ {/* Header */} +
+

Sohbet

+ +
+ + {/* Mesaj Modu */} +
+
+ + + + + {user.role === 'teacher' && ( + + )} +
+ + {messageMode === 'private' && ( + + )} +
+ + {/* Mesaj Listesi */} +
+ {chatMessages.length === 0 ? ( +
Henüz mesaj yok.
+ ) : ( + chatMessages.map((m) => ( +
+
+ {m.senderId !== user.id && ( +
+ {m.senderName} + {m.isTeacher && ' (Öğretmen)'} +
+ )} +
{m.message}
+
{formatTime(m.timestamp)}
+
+
+ )) + )} +
+
+ + {/* Mesaj Gönderme */} +
+
+ setNewMessage(e.target.value)} + placeholder="Mesaj yaz..." + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm" + /> + +
+
+
+ ) +} + +export default ChatPanel diff --git a/ui/src/components/classroom/DocumentsPanel.tsx b/ui/src/components/classroom/DocumentsPanel.tsx new file mode 100644 index 00000000..ed91bf81 --- /dev/null +++ b/ui/src/components/classroom/DocumentsPanel.tsx @@ -0,0 +1,153 @@ +import React, { useRef, useState } from 'react' +import { FaTimes, FaFile, FaEye, FaDownload, FaTrash } from 'react-icons/fa' +import { ClassDocumentDto } from '@/proxy/classroom/models' + +interface DocumentsPanelProps { + user: { role: string; name: string } + documents: ClassDocumentDto[] + onUpload: (file: File) => void + onDelete: (id: string) => void + onView: (doc: ClassDocumentDto) => void + onClose: () => void + formatFileSize: (bytes: number) => string + getFileIcon: (type: string) => JSX.Element +} + +const DocumentsPanel: React.FC = ({ + user, + documents, + onUpload, + onDelete, + onView, + onClose, + formatFileSize, + getFileIcon, +}) => { + const fileInputRef = useRef(null) + const [dragOver, setDragOver] = useState(false) + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setDragOver(false) + if (user.role !== 'teacher') return + const files = Array.from(e.dataTransfer.files) + files.forEach((file) => onUpload(file)) + } + + const handleFileSelect = (e: React.ChangeEvent) => { + if (user.role !== 'teacher') return + const files = Array.from(e.target.files || []) + files.forEach((file) => onUpload(file)) + if (fileInputRef.current) fileInputRef.current.value = '' + } + + return ( +
+ {/* Header */} +
+
+

Sınıf Dokümanları

+ +
+
+ + {/* Content */} +
+ {/* Upload Area (Teacher Only) */} + {user.role === 'teacher' && ( +
{ + 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')} +

+

{doc.uploadedBy}

+
+
+ +
+ + + + + + + {user.role === 'teacher' && ( + + )} +
+
+ ))} +
+ )} +
+
+ ) +} + +export default DocumentsPanel diff --git a/ui/src/components/classroom/LayoutPanel.tsx b/ui/src/components/classroom/LayoutPanel.tsx new file mode 100644 index 00000000..d81e9941 --- /dev/null +++ b/ui/src/components/classroom/LayoutPanel.tsx @@ -0,0 +1,112 @@ +import React from 'react' +import { FaTimes, FaTh, FaExpand, FaDesktop, FaUsers } from 'react-icons/fa' +import { VideoLayoutDto } from '@/proxy/classroom/models' + +interface LayoutPanelProps { + layouts: VideoLayoutDto[] + currentLayout: VideoLayoutDto + onChangeLayout: (layout: VideoLayoutDto) => void + onClose: () => void +} + +const getLayoutIcon = (type: string) => { + switch (type) { + case 'grid': + return + case 'speaker': + return + case 'presentation': + return + case 'sidebar': + return + case 'teacher-focus': + return + default: + return + } +} + +const LayoutPanel: React.FC = ({ + layouts, + currentLayout, + onChangeLayout, + onClose, +}) => { + return ( +
+
+
+

Video Layout Seçin

+ +
+
+ +
+
+ {layouts.map((layout) => ( + + ))} +
+
+
+ ) +} + +export default LayoutPanel diff --git a/ui/src/components/classroom/ParticipantsPanel.tsx b/ui/src/components/classroom/ParticipantsPanel.tsx new file mode 100644 index 00000000..e9b9efd7 --- /dev/null +++ b/ui/src/components/classroom/ParticipantsPanel.tsx @@ -0,0 +1,218 @@ +import React, { useState } from 'react' +import { + FaTimes, + FaUsers, + FaClipboardList, + FaHandPaper, + FaCheck, + FaMicrophone, + FaMicrophoneSlash, + FaVideoSlash, + FaUserTimes, +} from 'react-icons/fa' +import { ClassroomParticipantDto, ClassroomAttendanceDto } from '@/proxy/classroom/models' + +interface ParticipantsPanelProps { + user: { id: string; name: string; role: string } + participants: ClassroomParticipantDto[] + attendanceRecords: ClassroomAttendanceDto[] + onMuteParticipant: (participantId: string, isMuted: boolean, isTeacher: boolean) => void + onKickParticipant: (participantId: string) => void + onClose: () => void + formatTime: (timestamp: string) => string + formatDuration: (minutes: number) => string +} + +const ParticipantsPanel: React.FC = ({ + user, + participants, + attendanceRecords, + onMuteParticipant, + onKickParticipant, + onClose, + formatTime, + formatDuration, +}) => { + const [activeTab, setActiveTab] = useState<'participants' | 'attendance'>('participants') + + return ( +
+ {/* Header */} +
+
+

+ Katılımcılar ({participants.length + 1}) +

+ +
+ + {/* Tab Navigation */} +
+ + + {user.role === 'teacher' && ( + + )} +
+
+ + {/* Participants Tab */} + {activeTab === 'participants' && ( +
+
+ {/* Current User */} +
+
+
+ {user.name.charAt(0)} +
+ {user.name} (Siz) +
+ {user.role === 'teacher' && ( + + Öğretmen + + )} +
+ + {/* Other Participants */} + {participants.map((participant) => ( +
+
+
+ {participant.name.charAt(0)} +
+ {participant.name} + + {/* Hand Raise Indicator */} + {participant.isHandRaised && ( + + )} +
+ +
+ {/* Hand Raise Controls (Teacher Only) */} + {user.role === 'teacher' && + !participant.isTeacher && + participant.isHandRaised && ( + <> + + + + )} + + {/* Mute / Unmute Button */} + {user.role === 'teacher' && !participant.isTeacher && ( + + )} + + {/* Video muted indicator */} + {participant.isVideoMuted && } + + {participant.isTeacher && ( + + Öğretmen + + )} + + {/* Kick Button (Teacher Only) */} + {user.role === 'teacher' && !participant.isTeacher && ( + + )} +
+
+ ))} +
+
+ )} + + {/* Attendance Tab */} + {activeTab === 'attendance' && ( +
+ {attendanceRecords.length === 0 ? ( +
+ +

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

+
+ ) : ( +
+ {attendanceRecords.map((record) => ( +
+
+

{record.studentName}

+ + {formatDuration(record.totalDurationMinutes)} + +
+
+
Giriş: {formatTime(record.joinTime)}
+
+ Çıkış: {record.leaveTime ? formatTime(record.leaveTime) : 'Devam ediyor'} +
+
+
+ ))} +
+ )} +
+ )} +
+ ) +} + +export default ParticipantsPanel diff --git a/ui/src/components/classroom/SettingsPanel.tsx b/ui/src/components/classroom/SettingsPanel.tsx new file mode 100644 index 00000000..0d78ddf4 --- /dev/null +++ b/ui/src/components/classroom/SettingsPanel.tsx @@ -0,0 +1,164 @@ +import React from 'react' +import { FaTimes } from 'react-icons/fa' +import { ClassroomSettingsDto } from '@/proxy/classroom/models' + +interface SettingsPanelProps { + user: { role: string } + classSettings: ClassroomSettingsDto + onSettingsChange: (newSettings: Partial) => void + onClose: () => void +} + +const SettingsPanel: React.FC = ({ + user, + classSettings, + onSettingsChange, + onClose, +}) => { + return ( +
+
+
+

Sınıf Ayarları

+ +
+
+
+
+ {/* Katılımcı Davranışları */} +
+

Katılımcı İzinleri

+
+ + + + +
+
+ + {/* Varsayılan Ayarlar */} +
+

Varsayılan Ayarlar

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Otomatik Ayarlar */} +
+

Otomatik Ayarlar

+
+ +
+
+ + {user.role !== 'teacher' && ( +
+

⚠️ Ayarları sadece öğretmen değiştirebilir.

+
+ )} +
+
+
+ ) +} + +export default SettingsPanel diff --git a/ui/src/views/classroom/RoomDetail.tsx b/ui/src/views/classroom/RoomDetail.tsx index ecb376f4..4243e658 100644 --- a/ui/src/views/classroom/RoomDetail.tsx +++ b/ui/src/views/classroom/RoomDetail.tsx @@ -63,6 +63,11 @@ import { endClassroom } from '@/services/classroom.service' import { ROUTES_ENUM } from '@/routes/route.constant' import { Helmet } from 'react-helmet' import { useLocalization } from '@/utils/hooks/useLocalization' +import ChatPanel from '@/components/classroom/ChatPanel' +import ParticipantsPanel from '@/components/classroom/ParticipantsPanel' +import DocumentsPanel from '@/components/classroom/DocumentsPanel' +import LayoutPanel from '@/components/classroom/LayoutPanel' +import SettingsPanel from '@/components/classroom/SettingsPanel' type SidePanelType = | 'chat' @@ -659,822 +664,76 @@ const RoomDetail: React.FC = () => { const renderSidePanel = () => { if (!activeSidePanel) return null - const panelContent = () => { - switch (activeSidePanel) { - case 'chat': { - const availableRecipients = participants.filter((p) => p.id !== user.id) - return ( -
-
-
-

- Sınıf Sohbeti -

- -
-
+ switch (activeSidePanel) { + case 'chat': + return ( + setActiveSidePanel(null)} + formatTime={formatTime} + /> + ) - {/* Message Mode Selector */} -
-
- + case 'participants': + return ( + setActiveSidePanel(null)} + formatTime={formatTime} + formatDuration={formatDuration} + /> + ) - + case 'documents': + return ( + setActiveSidePanel(null)} + formatFileSize={formatFileSize} + getFileIcon={getFileIcon} + /> + ) - {user.role === 'teacher' && ( - - )} -
+ case 'layout': + return ( + setActiveSidePanel(null)} + /> + ) - {messageMode === 'private' && ( - - )} -
+ case 'settings': + return ( + setActiveSidePanel(null)} + /> + ) - {/* Messages */} -
- {chatMessages.length === 0 ? ( -
- Henüz mesaj bulunmamaktadır. -
- ) : ( - chatMessages.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} - /> - -
-
-
- ) - } - - case 'participants': { - return ( -
-
-
-

- Katılımcılar ({participants.length + 1}) -

- -
- - {/* Tab Navigation */} -
- - - {user.role === 'teacher' && ( - - )} -
-
- - {participantsActiveTab === 'participants' && ( - <> - {user.role === 'teacher' && participants.length > 0 && ( -
- -
- )} -
-
- {/* Current User */} -
-
-
- {user.name.charAt(0)} -
- {user.name} (Siz) -
-
- {user.role === 'teacher' && ( - - Öğretmen - - )} -
-
- - {/* Other Participants */} - {participants.map((participant) => ( -
-
-
- {participant.name.charAt(0)} -
- { - setMessageMode('private') - setSelectedRecipient({ id: participant.id, name: participant.name }) - setActiveSidePanel('chat') - }} - > - {participant.name} - - - {/* 👋 Parmak kaldırma göstergesi */} - {participant.isHandRaised && ( - - )} -
-
- {/* 🎓 Öğretmen için el kaldırma kontrolü */} - {user.role === 'teacher' && - !participant.isTeacher && - participant.isHandRaised && ( - <> - - - - )} - - {/* Ses aç/kapat butonu */} - {user.role === 'teacher' && !participant.isTeacher && ( - - )} - {/* Video durumu gösterimi */} - {participant.isVideoMuted && ( - - )} - {participant.isTeacher && ( - - Öğretmen - - )} - {user.role === 'teacher' && !participant.isTeacher && ( - - )} -
-
- ))} -
-
- - )} - - {participantsActiveTab === 'attendance' && ( -
- {attendanceRecords.length === 0 ? ( -
- -

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

-
- ) : ( -
- {attendanceRecords.map((record) => ( -
-
-

{record.studentName}

- - {formatDuration(record.totalDurationMinutes)} - -
-
-
Giriş: {formatTime(record.joinTime)}
-
- Çıkış:{' '} - {record.leaveTime ? formatTime(record.leaveTime) : 'Devam ediyor'} -
-
-
- ))} -
- )} -
- )} -
- ) - } - - case 'documents': - return ( -
-
-
-

Sınıf Dokümanları

- -
-
- -
- {/* Upload Area (Teacher Only) */} - {user.role === 'teacher' && ( -
{ - 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')} -

-

{doc.uploadedBy}

-
-
- -
- - - - - - - {user.role === 'teacher' && ( - - )} -
-
- ))} -
- )} -
-
- ) - - case 'layout': - return ( -
-
-
-

Video Layout Seçin

- -
-
- -
-
- {layouts.map((layout) => ( - - ))} -
-
-
- ) - - case 'settings': - return ( -
-
-
-

Sınıf Ayarları

- -
-
-
-
- {/* Katılımcı Davranışları */} -
-

Katılımcı İzinleri

-
- - - - -
-
- - {/* Varsayılan Ayarlar */} -
-

Varsayılan Ayarlar

-
-
- - -
-
- - -
-
- - -
-
-
- - {/* Otomatik Ayarlar */} -
-

Otomatik Ayarlar

-
- -
-
- - {user.role !== 'teacher' && ( -
-

- ⚠️ Ayarları sadece öğretmen değiştirebilir. -

-
- )} -
-
-
- ) - default: - return null - } + default: + return null } - - return ( -
- {panelContent()} - {/* Kapat butonu kaldırıldı, ana panelde gösterilecek */} -
- ) } return (