From e4544ad1e7cf5032d705768a8309553a9b883c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:39:09 +0300 Subject: [PATCH] Classroom backend ve ui --- .../Seeds/SeederData.json | 91 + ui/dev-dist/sw.js | 2 +- ui/package-lock.json | 182 +- ui/package.json | 1 + ui/src/components/classroom/ClassList.tsx | 941 ++++++++ ui/src/components/classroom/Dashboard.tsx | 38 + .../classroom/KickParticipantModal.tsx | 81 + .../classroom/Panels/AttendancePanel.tsx | 118 + .../components/classroom/Panels/ChatPanel.tsx | 249 ++ .../classroom/Panels/ClassLayoutPanel.tsx | 116 + .../classroom/Panels/DocumentPanel.tsx | 244 ++ .../classroom/Panels/HandRaisePanel.tsx | 113 + .../classroom/Panels/ScreenSharePanel.tsx | 76 + .../components/classroom/ParticipantGrid.tsx | 297 +++ ui/src/components/classroom/RoleSelector.tsx | 65 + ui/src/components/classroom/Room.tsx | 1998 +++++++++++++++++ ui/src/components/classroom/VideoPlayer.tsx | 72 + ui/src/proxy/classroom/data.ts | 47 + ui/src/proxy/classroom/models.ts | 135 ++ ui/src/routes/route.constant.ts | 5 + ui/src/services/classroom/signalr.ts | 394 ++++ ui/src/services/classroom/webrtc.ts | 150 ++ ui/src/store/auth.model.ts | 3 + ui/src/utils/hooks/useClassroomLogic.ts | 68 + ui/src/views/classroom/ClassListPage.tsx | 15 + ui/src/views/classroom/DashboardPage.tsx | 8 + ui/src/views/classroom/RoomPage.tsx | 23 + 27 files changed, 5530 insertions(+), 2 deletions(-) create mode 100644 ui/src/components/classroom/ClassList.tsx create mode 100644 ui/src/components/classroom/Dashboard.tsx create mode 100644 ui/src/components/classroom/KickParticipantModal.tsx create mode 100644 ui/src/components/classroom/Panels/AttendancePanel.tsx create mode 100644 ui/src/components/classroom/Panels/ChatPanel.tsx create mode 100644 ui/src/components/classroom/Panels/ClassLayoutPanel.tsx create mode 100644 ui/src/components/classroom/Panels/DocumentPanel.tsx create mode 100644 ui/src/components/classroom/Panels/HandRaisePanel.tsx create mode 100644 ui/src/components/classroom/Panels/ScreenSharePanel.tsx create mode 100644 ui/src/components/classroom/ParticipantGrid.tsx create mode 100644 ui/src/components/classroom/RoleSelector.tsx create mode 100644 ui/src/components/classroom/Room.tsx create mode 100644 ui/src/components/classroom/VideoPlayer.tsx create mode 100644 ui/src/proxy/classroom/data.ts create mode 100644 ui/src/proxy/classroom/models.ts create mode 100644 ui/src/services/classroom/signalr.ts create mode 100644 ui/src/services/classroom/webrtc.ts create mode 100644 ui/src/utils/hooks/useClassroomLogic.ts create mode 100644 ui/src/views/classroom/ClassListPage.tsx create mode 100644 ui/src/views/classroom/DashboardPage.tsx create mode 100644 ui/src/views/classroom/RoomPage.tsx diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json index e1afa0a9..32ce0d2b 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json @@ -99,6 +99,10 @@ { "Name": "App.Contact", "DisplayName": "App.Contact" + }, + { + "Name": "App.Classroom", + "DisplayName": "App.Classroom" } ], "PermissionDefinitionRecords": [ @@ -2799,6 +2803,38 @@ "DisplayName": "Import", "IsEnabled": true, "MultiTenancySide": 2 + }, + { + "GroupName": "App.Classroom", + "Name": "App.Classroom", + "ParentName": null, + "DisplayName": "App.Classroom", + "IsEnabled": true, + "MultiTenancySide": 2 + }, + { + "GroupName": "App.Classroom", + "Name": "App.Classroom.Dashboard", + "ParentName": "App.Classroom", + "DisplayName": "App.Classroom.Dashboard", + "IsEnabled": true, + "MultiTenancySide": 2 + }, + { + "GroupName": "App.Classroom", + "Name": "App.Classroom.List", + "ParentName": "App.Classroom", + "DisplayName": "App.Classroom.List", + "IsEnabled": true, + "MultiTenancySide": 2 + }, + { + "GroupName": "App.Classroom", + "Name": "App.Classroom.RoomDetail", + "ParentName": "App.Classroom", + "DisplayName": "App.Classroom.RoomDetail", + "IsEnabled": true, + "MultiTenancySide": 2 } ], "Menus": [ @@ -3501,6 +3537,16 @@ "Icon": "FaSynagogue", "RequiredPermissionName": "App.Definitions.UomCategory", "IsDisabled": false + }, + { + "ParentCode": "App.Administration", + "Code": "App.Classroom", + "DisplayName": "App.Classroom", + "Order": 5, + "Url": "/admin/classroom/dashboard", + "Icon": "FcNeutralDecision", + "RequiredPermissionName": "App.Classroom.Dashboard", + "IsDisabled": false } ], "Routes": [ @@ -3874,6 +3920,27 @@ "componentPath": "@/views/report/ReportViewerPage", "routeType": "protected", "authority": ["App.Reports.Categories"] + }, + { + "key": "admin.classroom.dashboard", + "path": "/admin/classroom/dashboard", + "componentPath": "@/views/classroom/DashboardPage", + "routeType": "protected", + "authority": ["App.Classroom.Dashboard"] + }, + { + "key": "admin.classroom.classes", + "path": "/admin/classroom/classes", + "componentPath": "@/views/classroom/ClassListPage", + "routeType": "protected", + "authority": ["App.Classroom.List"] + }, + { + "key": "admin.classroom.classroom", + "path": "/admin/classroom/room/:id", + "componentPath": "@/views/classroom/RoomPage", + "routeType": "protected", + "authority": ["App.Classroom.RoomDetail"] } ], "Languages": [ @@ -14095,6 +14162,30 @@ "key": "App.Contact", "tr": "İletişim", "en": "Contact" + }, + { + "resourceName": "Platform", + "key": "App.Classroom", + "tr": "Sınıf", + "en": "Classroom" + }, + { + "resourceName": "Platform", + "key": "App.Classroom.Dashboard", + "tr": "Gösterge Paneli", + "en": "Dashboard" + }, + { + "resourceName": "Platform", + "key": "App.Classroom.List", + "tr": "Sınıflar", + "en": "Classes" + }, + { + "resourceName": "Platform", + "key": "App.Classroom.RoomDetail", + "tr": "Virtul Classroom", + "en": "Sanal Sınıf" } ], "Settings": [ diff --git a/ui/dev-dist/sw.js b/ui/dev-dist/sw.js index d283fbca..5c49a2d0 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.c7pq42r4d5g" + "revision": "0.9qu602jrc3g" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/ui/package-lock.json b/ui/package-lock.json index 8af9af75..07271747 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -22,6 +22,7 @@ "@fullcalendar/react": "^6.1.8", "@fullcalendar/timegrid": "^6.1.8", "@marsidev/react-turnstile": "^0.2.1", + "@microsoft/signalr": "^9.0.6", "@monaco-editor/react": "^4.6.0", "@tanstack/react-query": "^4.29.19", "@tanstack/react-table": "^8.8.5", @@ -2714,6 +2715,19 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@microsoft/signalr": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-9.0.6.tgz", + "integrity": "sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, "node_modules/@monaco-editor/loader": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", @@ -3985,6 +3999,18 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -6687,11 +6713,29 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exceljs": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", @@ -6804,6 +6848,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -8770,6 +8824,48 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -9871,15 +9967,32 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10460,6 +10573,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -10709,6 +10828,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -11687,6 +11812,30 @@ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -12144,6 +12293,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", @@ -12921,6 +13080,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/ui/package.json b/ui/package.json index dbd891fb..2255408f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,6 +15,7 @@ "format": "npm run prettier:fix && npm run lint:fix" }, "dependencies": { + "@microsoft/signalr": "^9.0.6", "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.0", "@babel/standalone": "^7.28.0", diff --git a/ui/src/components/classroom/ClassList.tsx b/ui/src/components/classroom/ClassList.tsx new file mode 100644 index 00000000..e1529466 --- /dev/null +++ b/ui/src/components/classroom/ClassList.tsx @@ -0,0 +1,941 @@ +import React, { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { motion } from 'framer-motion' +import { + FaPlus, + FaCalendarAlt, + FaClock, + FaUsers, + FaPlay, + FaEdit, + FaTrash, + FaEye, +} from 'react-icons/fa' +import { ClassroomDto } from '@/proxy/classroom/models' +import { initialScheduledClasses } from '@/proxy/classroom/data' +import { useStoreState } from '@/store/store' +import { ROUTES_ENUM } from '@/routes/route.constant' + +interface DashboardProps { + onCreateClass: (classData: Partial) => void + onJoinClass: (classSession: ClassroomDto) => void + onEditClass: (classId: string, classData: Partial) => void + onDeleteClass: (classId: string) => void +} + +export const ClassList: React.FC = ({ + onCreateClass, + onJoinClass, + onEditClass, + onDeleteClass, +}) => { + const navigate = useNavigate() + const { user } = useStoreState((state) => state.auth) + + const [showCreateModal, setShowCreateModal] = useState(false) + const [showEditModal, setShowEditModal] = useState(false) + const [editingClass, setEditingClass] = useState(null) + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [deletingClass, setDeletingClass] = useState(null) + const [scheduledClasses, setScheduledClasses] = useState(initialScheduledClasses) + + const [formData, setFormData] = useState({ + name: '', + description: '', + subject: '', + scheduledStartTime: '', + duration: 60, + maxParticipants: 30, + settings: { + allowHandRaise: true, + defaultMicrophoneState: 'muted' as 'muted' | 'unmuted', + defaultCameraState: 'on' as 'on' | 'off', + defaultLayout: 'grid', + allowStudentScreenShare: false, + allowStudentChat: true, + allowPrivateMessages: true, + autoMuteNewParticipants: true, + recordSession: false, + waitingRoomEnabled: false, + }, + }) + + const canJoinClass = (scheduledTime: string) => { + const scheduled = new Date(scheduledTime) + const now = new Date() + const tenMinutesBefore = new Date(scheduled.getTime() - 10 * 60 * 1000) + const twoHoursAfter = new Date(scheduled.getTime() + 2 * 60 * 60 * 1000) // 2 saat sonrasına kadar + return now >= tenMinutesBefore && now <= twoHoursAfter + } + + const getTimeUntilClass = (scheduledTime: string) => { + const scheduled = new Date(scheduledTime) + const now = new Date() + const diff = scheduled.getTime() - now.getTime() + + if (diff <= 0) { + // Sınıf başladıysa, ne kadar süredir devam ettiğini göster + const elapsed = Math.abs(diff) + const elapsedMinutes = Math.floor(elapsed / (1000 * 60)) + if (elapsedMinutes < 60) { + return `${elapsedMinutes} dakikadır devam ediyor` + } + const elapsedHours = Math.floor(elapsedMinutes / 60) + const remainingMinutes = elapsedMinutes % 60 + return `${elapsedHours}s ${remainingMinutes}d devam ediyor` + } + + const hours = Math.floor(diff / (1000 * 60 * 60)) + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) + + if (hours > 0) { + return `${hours}s ${minutes}d kaldı` + } + return `${minutes}d kaldı` + } + + const handleCreateClass = (e: React.FormEvent) => { + e.preventDefault() + const newClass: Partial = { + ...formData, + id: `class-${Date.now()}`, + teacherId: user.id, + teacherName: user.name, + isActive: false, + isScheduled: true, + participantCount: 0, + } + + onCreateClass(newClass) + setScheduledClasses((prev) => [...prev, newClass as ClassroomDto]) + setShowCreateModal(false) + setFormData({ + name: '', + description: '', + subject: '', + scheduledStartTime: '', + duration: 60, + maxParticipants: 30, + settings: { + allowHandRaise: true, + defaultMicrophoneState: 'muted', + defaultCameraState: 'on', + defaultLayout: 'grid', + allowStudentScreenShare: false, + allowStudentChat: true, + allowPrivateMessages: true, + autoMuteNewParticipants: true, + recordSession: false, + waitingRoomEnabled: false, + }, + }) + // Yeni oluşturulan sınıfa yönlendir + if (newClass.id) { + navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', newClass.id)) + } + } + + const handleEditClass = (e: React.FormEvent) => { + e.preventDefault() + if (!editingClass) return + + const updatedClass = { + ...editingClass, + ...formData, + } + + setScheduledClasses((prev) => prev.map((c) => (c.id === editingClass.id ? updatedClass : c))) + onEditClass(editingClass.id, formData) + setShowEditModal(false) + setEditingClass(null) + resetForm() + } + + const handleDeleteClass = () => { + if (!deletingClass) return + + setScheduledClasses((prev) => prev.filter((c) => c.id !== deletingClass.id)) + onDeleteClass(deletingClass.id) + setShowDeleteModal(false) + setDeletingClass(null) + } + + const openEditModal = (classSession: ClassroomDto) => { + setEditingClass(classSession) + setFormData({ + name: classSession.name, + description: classSession.description || '', + subject: classSession.subject || '', + scheduledStartTime: new Date(classSession.scheduledStartTime).toISOString().slice(0, 16), + duration: classSession.duration || 60, + maxParticipants: classSession.maxParticipants || 30, + settings: classSession.settings || { + allowHandRaise: true, + defaultMicrophoneState: 'muted', + defaultCameraState: 'on', + defaultLayout: 'grid', + allowStudentScreenShare: false, + allowStudentChat: true, + allowPrivateMessages: true, + autoMuteNewParticipants: true, + recordSession: false, + waitingRoomEnabled: false, + }, + }) + setShowEditModal(true) + } + + const openDeleteModal = (classSession: ClassroomDto) => { + setDeletingClass(classSession) + setShowDeleteModal(true) + } + + const resetForm = () => { + setFormData({ + name: '', + description: '', + subject: '', + scheduledStartTime: '', + duration: 60, + maxParticipants: 30, + settings: { + allowHandRaise: true, + defaultMicrophoneState: 'muted', + defaultCameraState: 'on', + defaultLayout: 'grid', + allowStudentScreenShare: false, + allowStudentChat: true, + allowPrivateMessages: true, + autoMuteNewParticipants: true, + recordSession: false, + waitingRoomEnabled: false, + }, + }) + } + + const handleStartClass = (classSession: ClassroomDto) => { + const updatedClass = { + ...classSession, + isActive: true, + startTime: new Date().toISOString(), + } + + setScheduledClasses((prev) => prev.map((c) => (c.id === classSession.id ? updatedClass : c))) + onJoinClass(updatedClass) + // Sınıf başlatıldığında classroom ekranına yönlendir + if (updatedClass.id) { + navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', updatedClass.id)) + } + } + + const formatDateTime = (dateString: string) => { + return new Date(dateString).toLocaleString('tr-TR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } + + return ( +
+ {/* Header */} +
+
+
+
+

+ Sanal Sınıf Dashboard +

+

Hoş geldiniz, {user.name}

+
+ {user.role === 'teacher' && ( + + )} +
+
+
+ + {/* Main Content */} +
+ {/* Stats Cards */} +
+ +
+
+ +
+
+

Toplam Sınıf

+

+ {scheduledClasses.length} +

+
+
+
+ + +
+
+ +
+
+

Aktif Sınıf

+

+ {scheduledClasses.filter((c) => c.isActive).length} +

+
+
+
+ + +
+
+ +
+
+

Toplam Katılımcı

+

+ {scheduledClasses.reduce((sum, c) => sum + c.participantCount, 0)} +

+
+
+
+
+ + {/* Scheduled Classes */} +
+
+

Programlı Sınıflar

+
+
+ {scheduledClasses.length === 0 ? ( +
+ +

Henüz programlanmış sınıf bulunmamaktadır.

+
+ ) : ( +
+ {scheduledClasses.map((classSession, index) => ( + +
+
+
+

+ {classSession.name} +

+ + {classSession.isActive + ? 'Aktif' + : canJoinClass(classSession.scheduledStartTime) + ? 'Katılım Açık' + : 'Beklemede'} + +
+ +

+ {classSession.description} +

+ +
+
+ + + {formatDateTime(classSession.scheduledStartTime)} + +
+
+ + {classSession.duration} dakika +
+
+ + + {classSession.participantCount}/{classSession.maxParticipants} + +
+
+ + + {getTimeUntilClass(classSession.scheduledStartTime)} + +
+
+
+ +
+ {user.role === 'teacher' && classSession.teacherId === user.id && ( +
+ + +
+ )} + + {canJoinClass(classSession.scheduledStartTime) && ( + + )} +
+
+
+ ))} +
+ )} +
+
+
+ + {/* Create Class Modal */} + {showCreateModal && ( +
+ +
+

Yeni Sınıf Oluştur

+
+ +
+
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Örn: Matematik 101 - Diferansiyel Denklemler" + /> +
+ +
+ +