Classroom canlı sisteme alınması

Classroom Listesi, NewClassroom, EditClassroom, DeleteClassroom
This commit is contained in:
Sedat Öztürk 2025-08-27 00:42:01 +03:00
parent 6365a0672f
commit 87f04ca653
20 changed files with 624 additions and 1072 deletions

View file

@ -1,4 +1,6 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Volo.Abp.Application.Dtos;
namespace Kurs.Platform.Classrooms;
@ -19,26 +21,32 @@ public class ClassroomDto : FullAuditedEntityDto<Guid>
public bool IsScheduled { get; set; }
public int ParticipantCount { get; set; }
public bool CanJoin { get; set; }
[JsonIgnore]
public string SettingsJson { get; set; }
public ClassroomSettingsDto SettingsDto
{
get
{
if (!string.IsNullOrEmpty(SettingsJson))
return JsonSerializer.Deserialize<ClassroomSettingsDto>(SettingsJson);
return new ClassroomSettingsDto();
}
set { SettingsJson = JsonSerializer.Serialize(value); }
}
}
public class CreateClassroomDto
public class ClassroomSettingsDto
{
public string Name { get; set; }
public string Description { get; set; }
public string Subject { get; set; }
public DateTime ScheduledStartTime { get; set; }
public int Duration { get; set; } = 60;
public int MaxParticipants { get; set; } = 30;
}
public class UpdateClassroomDto
{
public string Name { get; set; }
public string Description { get; set; }
public string Subject { get; set; }
public DateTime ScheduledStartTime { get; set; }
public int Duration { get; set; }
public int MaxParticipants { get; set; }
public bool AllowHandRaise { get; set; }
public bool AllowStudentChat { get; set; }
public bool AllowPrivateMessages { get; set; }
public bool AllowStudentScreenShare { get; set; }
public string DefaultMicrophoneState { get; set; } = "muted"; // 'muted' | 'unmuted'
public string DefaultCameraState { get; set; } = "off"; // 'on' | 'off'
public string DefaultLayout { get; set; } = "grid";
public bool AutoMuteNewParticipants { get; set; }
}
public class GetClassroomListDto : PagedAndSortedResultRequestDto

View file

@ -8,10 +8,10 @@ namespace Kurs.Platform.Classrooms;
public interface IClassroomAppService : IApplicationService
{
Task<ClassroomDto> CreateAsync(CreateClassroomDto input);
Task<PagedResultDto<ClassroomDto>> GetListAsync(GetClassroomListDto input);
Task<ClassroomDto> CreateAsync(ClassroomDto input);
Task<PagedResultDto<ClassroomDto>> GetListAsync(PagedAndSortedResultRequestDto input);
Task<ClassroomDto> GetAsync(Guid id);
Task<ClassroomDto> UpdateAsync(Guid id, UpdateClassroomDto input);
Task<ClassroomDto> UpdateAsync(Guid id, ClassroomDto input);
Task DeleteAsync(Guid id);
Task<ClassroomDto> StartClassAsync(Guid id);
Task EndClassAsync(Guid id);

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Kurs.Platform.Entities;
using Microsoft.AspNetCore.Authorization;
@ -26,40 +27,15 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
_attendanceRepository = attendanceRepository;
}
public async Task<ClassroomDto> CreateAsync(CreateClassroomDto input)
public async Task<ClassroomDto> GetAsync(Guid id)
{
var classSession = new Classroom(
GuidGenerator.Create(),
input.Name,
input.Description,
input.Subject,
CurrentUser.Id,
CurrentUser.Name,
input.ScheduledStartTime,
input.Duration,
input.MaxParticipants
);
await _classSessionRepository.InsertAsync(classSession);
await CurrentUnitOfWork.SaveChangesAsync();
var classSession = await _classSessionRepository.GetAsync(id);
return ObjectMapper.Map<Classroom, ClassroomDto>(classSession);
}
public async Task<PagedResultDto<ClassroomDto>> GetListAsync(GetClassroomListDto input)
public async Task<PagedResultDto<ClassroomDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
var query = await _classSessionRepository.GetQueryableAsync();
if (input.IsActive.HasValue)
{
query = query.Where(x => x.IsActive == input.IsActive.Value);
}
if (input.TeacherId.HasValue)
{
query = query.Where(x => x.TeacherId == input.TeacherId.Value);
}
var totalCount = query.Count();
var items = query
.OrderBy(x => x.ScheduledStartTime)
@ -73,13 +49,29 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
);
}
public async Task<ClassroomDto> GetAsync(Guid id)
public async Task<ClassroomDto> CreateAsync(ClassroomDto input)
{
var classSession = await _classSessionRepository.GetAsync(id);
var classSession = new Classroom(
GuidGenerator.Create(),
input.Name,
input.Description,
input.Subject,
CurrentUser.Id,
CurrentUser.Name,
input.ScheduledStartTime,
input.Duration,
input.MaxParticipants,
false,
true,
input.SettingsJson = JsonSerializer.Serialize(input.SettingsDto)
);
await _classSessionRepository.InsertAsync(classSession);
return ObjectMapper.Map<Classroom, ClassroomDto>(classSession);
}
public async Task<ClassroomDto> UpdateAsync(Guid id, UpdateClassroomDto input)
public async Task<ClassroomDto> UpdateAsync(Guid id, ClassroomDto input)
{
var classSession = await _classSessionRepository.GetAsync(id);

View file

@ -10,9 +10,6 @@ public class ClassroomAutoMapperProfile : Profile
CreateMap<Classroom, ClassroomDto>()
.ForMember(dest => dest.CanJoin, opt => opt.MapFrom(src => src.CanJoin()));
CreateMap<CreateClassroomDto, Classroom>();
CreateMap<UpdateClassroomDto, Classroom>();
CreateMap<ClassAttandance, ClassAttendanceDto>();
CreateMap<ClassParticipant, ClassParticipantDto>();
CreateMap<ClassChat, ClassChatDto>();

View file

@ -14,11 +14,12 @@ public class Classroom : FullAuditedEntity<Guid>
public DateTime ScheduledStartTime { get; set; }
public DateTime? ActualStartTime { get; set; }
public DateTime? EndTime { get; set; }
public int Duration { get; set; } // minutes
public int Duration { get; set; }
public int MaxParticipants { get; set; }
public bool IsActive { get; set; }
public bool IsScheduled { get; set; }
public int ParticipantCount { get; set; }
public string SettingsJson { get; set; }
public virtual ICollection<ClassParticipant> Participants { get; set; }
public virtual ICollection<ClassAttandance> AttendanceRecords { get; set; }
@ -40,7 +41,10 @@ public class Classroom : FullAuditedEntity<Guid>
string teacherName,
DateTime scheduledStartTime,
int duration,
int maxParticipants
int maxParticipants,
bool isActive,
bool isScheduled,
string settingsJson
) : base(id)
{
Name = name;
@ -51,9 +55,9 @@ public class Classroom : FullAuditedEntity<Guid>
ScheduledStartTime = scheduledStartTime;
Duration = duration;
MaxParticipants = maxParticipants;
IsActive = false;
IsScheduled = true;
ParticipantCount = 0;
IsActive = isActive;
IsScheduled = isScheduled;
SettingsJson = settingsJson;
Participants = new HashSet<ClassParticipant>();
AttendanceRecords = new HashSet<ClassAttandance>();

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20250826055306_Initial")]
[Migration("20250826203853_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -1840,6 +1840,9 @@ namespace Kurs.Platform.Migrations
b.Property<DateTime>("ScheduledStartTime")
.HasColumnType("datetime2");
b.Property<string>("SettingsJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("Subject")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");

View file

@ -804,6 +804,7 @@ namespace Kurs.Platform.Migrations
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsScheduled = table.Column<bool>(type: "bit", nullable: false),
ParticipantCount = table.Column<int>(type: "int", nullable: false),
SettingsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),

View file

@ -1837,6 +1837,9 @@ namespace Kurs.Platform.Migrations
b.Property<DateTime>("ScheduledStartTime")
.HasColumnType("datetime2");
b.Property<string>("SettingsJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("Subject")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");

View file

@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.lcv915aoevo"
"revision": "0.8cs01edqoio"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

711
ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,6 @@
"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",
@ -30,6 +29,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",

View file

@ -1,63 +0,0 @@
import React from 'react'
import { motion } from 'framer-motion'
import { FaGraduationCap, FaUserCheck, FaEye } from 'react-icons/fa'
import { Role } from '@/proxy/classroom/models'
interface RoleSelectorProps {
onRoleSelect: (role: Role) => void
}
export const RoleSelector: React.FC<RoleSelectorProps> = ({ onRoleSelect }) => {
return (
<div className="flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center w-full max-w-4xl"
>
<p className="text-lg sm:text-xl text-gray-600 mb-8 sm:mb-12">Lütfen rolünüzü seçin</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onRoleSelect('teacher')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-blue-500"
>
<FaGraduationCap size={48} className="mx-auto text-blue-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğretmen</h2>
<p className="text-gray-600 text-sm sm:text-base">
Ders başlatın, öğrencilerle iletişim kurun ve katılım raporlarını görün
</p>
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onRoleSelect('student')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-green-500"
>
<FaUserCheck size={48} className="mx-auto text-green-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğrenci</h2>
<p className="text-gray-600 text-sm sm:text-base">
Aktif derslere katılın, öğretmeniniz ve diğer öğrencilerle etkileşim kurun
</p>
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onRoleSelect('observer')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-purple-500 md:col-span-2 lg:col-span-1"
>
<FaEye size={48} className="mx-auto text-purple-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Gözlemci</h2>
<p className="text-gray-600 text-sm sm:text-base">
Sınıfı gözlemleyin, eğitim sürecini takip edin (ses/video paylaşımı yok)
</p>
</motion.button>
</div>
</motion.div>
</div>
)
}

View file

@ -1,47 +0,0 @@
export const initialScheduledClasses = [
{
id: '1',
name: 'Matematik 101 - Diferansiyel Denklemler',
description: 'İleri matematik konuları ve uygulamaları',
teacherId: '0cc64443-fe3b-5f2c-bb22-3a1bf7acd82a',
teacherName: 'Prof. Dr. Mehmet Özkan',
scheduledStartTime: new Date(Date.now() - 300000).toISOString(), // 5 minutes ago (can join)
startTime: '',
isActive: false,
isScheduled: true,
participantCount: 0,
maxParticipants: 30,
subject: 'Matematik',
duration: 90,
},
{
id: '2',
name: 'Fizik 201 - Kuantum Mekaniği',
description: 'Modern fizik ve kuantum teorisi temelleri',
teacherId: 'teacher2',
teacherName: 'Dr. Ayşe Kaya',
scheduledStartTime: new Date(Date.now() + 1800000).toISOString(), // 30 minutes from now
startTime: '',
isActive: false,
isScheduled: true,
participantCount: 0,
maxParticipants: 25,
subject: 'Fizik',
duration: 120,
},
{
id: '3',
name: 'Kimya 301 - Organik Kimya',
description: 'Organik bileşikler ve reaksiyon mekanizmaları',
teacherId: 'current-teacher',
teacherName: 'Dr. Ali Veli',
scheduledStartTime: new Date(Date.now() - 120000).toISOString(), // 2 minutes ago (can join)
startTime: '',
isActive: false,
isScheduled: true,
participantCount: 0,
maxParticipants: 20,
subject: 'Kimya',
duration: 75,
},
]

View file

@ -13,31 +13,30 @@ export interface ClassroomDto {
id: string
name: string
description?: string
subject?: string
teacherId: string
teacherName: string
startTime: string
scheduledStartTime: string
actualStartTime?: string
endTime?: string
duration?: number
maxParticipants?: number
isActive: boolean
isScheduled: boolean
participantCount: number
maxParticipants?: number
subject?: string
duration?: number // in minutes
canJoin: boolean
settings?: ClassroomSettingsDto
}
export interface ClassroomSettingsDto {
allowHandRaise: boolean
allowStudentChat: boolean
allowPrivateMessages: boolean
allowStudentScreenShare: boolean
defaultMicrophoneState: 'muted' | 'unmuted'
defaultCameraState: 'on' | 'off'
defaultLayout: string
allowStudentScreenShare: boolean
allowStudentChat: boolean
allowPrivateMessages: boolean
autoMuteNewParticipants: boolean
recordSession: boolean
waitingRoomEnabled: boolean
}
export interface ClassAttendanceDto {

View file

@ -1,5 +1,6 @@
import { ClassroomDto } from '@/proxy/classroom/models'
import apiService from './api.service'
import { PagedAndSortedResultRequestDto, PagedResultDto } from '@/proxy'
export const getClassroomById = (id: string) =>
apiService.fetchData<ClassroomDto>({
@ -8,47 +9,29 @@ export const getClassroomById = (id: string) =>
params: { id },
})
export const getClassrooms = () =>
apiService.fetchData<ClassroomDto[]>({
export const getClassrooms = (input: PagedAndSortedResultRequestDto) =>
apiService.fetchData<PagedResultDto<ClassroomDto>>({
method: 'GET',
url: `/api/app/classroom`,
params: input,
})
// export const getChartOptions = (chartCode: string) =>
// apiService.fetchData<ChartDto>({
// method: 'GET',
// url: `/api/app/charts/chart-options`,
// params: { chartCode },
// })
export const createClassroom = (input: ClassroomDto) =>
apiService.fetchData<ClassroomDto>({
method: 'POST',
url: `/api/app/classroom`,
data: input as any,
})
// export const getChartSelect = (chartCode: string, filter?: string) =>
// apiService.fetchData({
// method: 'GET',
// url: `/api/app/chart-select/select`,
// params: { chartCode, filter },
// })
export const updateClassroom = (input: ClassroomDto) =>
apiService.fetchData({
method: 'PUT',
url: `/api/app/classroom/${input.id}`,
data: input,
})
// export const putCharts = (input: ChartEditDto) =>
// apiService.fetchData({
// method: 'PUT',
// url: `/api/app/charts/${input.id}`,
// data: input,
// })
// export const deleteChartJsonItem = (
// id: string,
// chartCode: string,
// index: number,
// fieldName: string,
// ) =>
// apiService.fetchData({
// method: 'DELETE',
// url: `/api/app/charts/chart-json-item?id=${id}&chartCode=${chartCode}&index=${index}&fieldName=${fieldName}`,
// })
// export const putChartJsonItem = (input: ChartJsonItemRowDto) =>
// apiService.fetchData({
// method: 'PUT',
// url: `/api/app/charts/chart-json-item`,
// data: input,
// })
export const deleteClassroom = (id: string) =>
apiService.fetchData({
method: 'DELETE',
url: `/api/app/classroom/${id}`,
})

View file

@ -9,7 +9,7 @@ import * as signalR from '@microsoft/signalr'
export class SignalRService {
private connection!: signalR.HubConnection
private isConnected: boolean = false
private demoMode: boolean = true // Start in demo mode by default
private demoMode: boolean = false // Start in demo mode by default
private onSignalingMessage?: (message: SignalingMessageDto) => void
private onAttendanceUpdate?: (record: ClassAttendanceDto) => void
private onParticipantJoined?: (userId: string, name: string) => void

View file

@ -18,16 +18,6 @@ export function useClassroomLogic() {
setRoleState('dashboard')
}
const handleJoinClass = (classSession: ClassroomDto, userName?: string) => {
setCurrentClass(classSession)
setRoleState('classroom')
}
const handleLeaveClass = () => {
setCurrentClass(null)
setRoleState('dashboard')
}
const handleCreateClass = (classData: Partial<ClassroomDto>) => {
const newClass = {
...classData,
@ -57,8 +47,6 @@ export function useClassroomLogic() {
allClasses,
setAllClasses,
handleRoleSelect,
handleJoinClass,
handleLeaveClass,
handleCreateClass,
handleEditClass,
handleDeleteClass,

View file

@ -13,24 +13,21 @@ import {
} 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'
import { Container } from '@/components/shared'
import { getClassrooms } from '@/services/classroom.service'
import {
createClassroom,
deleteClassroom,
getClassrooms,
updateClassroom,
} from '@/services/classroom.service'
const ClassList: React.FC = () => {
const navigate = useNavigate()
const { user } = useStoreState((state) => state.auth)
const [showCreateModal, setShowCreateModal] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [editingClass, setEditingClass] = useState<ClassroomDto | null>(null)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [deletingClass, setDeletingClass] = useState<ClassroomDto | null>(null)
const [scheduledClasses, setScheduledClasses] = useState<ClassroomDto[]>(initialScheduledClasses)
const [formData, setFormData] = useState({
const newClassEntity = {
name: '',
description: '',
subject: '',
@ -39,17 +36,133 @@ const ClassList: React.FC = () => {
maxParticipants: 30,
settings: {
allowHandRaise: true,
defaultMicrophoneState: 'muted' as 'muted' | 'unmuted',
defaultCameraState: 'on' as 'on' | 'off',
defaultLayout: 'grid',
allowStudentScreenShare: false,
allowStudentChat: true,
allowPrivateMessages: true,
allowStudentScreenShare: false,
defaultMicrophoneState: 'muted',
defaultCameraState: 'on',
defaultLayout: 'grid',
autoMuteNewParticipants: true,
recordSession: false,
waitingRoomEnabled: false,
},
})
}
const [classList, setClassList] = useState<ClassroomDto[]>([])
const [newClassroom, setNewClassroom] = useState(newClassEntity)
const [editingClass, setEditingClass] = useState<ClassroomDto | null>(null)
const [deletingClass, setDeletingClass] = useState<ClassroomDto | null>(null)
const [showCreateModal, setShowCreateModal] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const getClassroomList = async (skipCount = 0, maxResultCount = 1000, sorting = '') => {
try {
const result = await getClassrooms({
sorting,
skipCount,
maxResultCount,
})
setClassList(result.data.items || [])
} catch (error) {
console.error('Error fetching classrooms:', error)
}
}
useEffect(() => {
getClassroomList()
}, [])
const handleCreateClass = async (e: React.FormEvent) => {
e.preventDefault()
const newClass: ClassroomDto = {
...newClassroom,
id: crypto.randomUUID(),
teacherId: user.id,
teacherName: user.name,
isActive: false,
isScheduled: true,
participantCount: 0,
actualStartTime: '',
canJoin: false,
settings: {
...newClassroom.settings,
defaultMicrophoneState: newClassroom.settings.defaultMicrophoneState as 'muted' | 'unmuted',
defaultCameraState: newClassroom.settings.defaultCameraState as 'on' | 'off',
},
}
try {
await createClassroom(newClass)
getClassroomList()
setShowCreateModal(false)
setNewClassroom(newClassEntity)
if (newClass.id) {
navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', newClass.id))
}
} catch (error) {
console.error('Sınıf oluştururken hata oluştu:', error)
}
}
const handleEditClass = async (e: React.FormEvent) => {
e.preventDefault()
if (!editingClass) return
try {
await updateClassroom(editingClass)
getClassroomList()
setShowEditModal(false)
setEditingClass(null)
resetForm()
} catch (error) {
console.error('Sınıf oluştururken hata oluştu:', error)
}
}
const openEditModal = (classSession: ClassroomDto) => {
setEditingClass(classSession)
setShowEditModal(true)
}
const openDeleteModal = (classSession: ClassroomDto) => {
setDeletingClass(classSession)
setShowDeleteModal(true)
}
const handleDeleteClass = async () => {
if (!deletingClass) return
try {
await deleteClassroom(deletingClass.id)
getClassroomList()
setShowDeleteModal(false)
setDeletingClass(null)
} catch (error) {
console.error('Sınıf silinirken hata oluştu:', error)
}
}
const resetForm = () => {
setNewClassroom(newClassEntity)
}
const handleStartClass = (classSession: ClassroomDto) => {
const updatedClass = {
...classSession,
isActive: true,
actualStartTime: new Date().toISOString(),
}
getClassroomList()
if (updatedClass.id) {
navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', updatedClass.id))
}
}
const canJoinClass = (scheduledTime: string) => {
const scheduled = new Date(scheduledTime)
@ -85,136 +198,6 @@ const ClassList: React.FC = () => {
return `${minutes}d kaldı`
}
const handleCreateClass = (e: React.FormEvent) => {
e.preventDefault()
const newClass: Partial<ClassroomDto> = {
...formData,
id: `class-${Date.now()}`,
teacherId: user.id,
teacherName: user.name,
isActive: false,
isScheduled: true,
participantCount: 0,
}
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)))
setShowEditModal(false)
setEditingClass(null)
resetForm()
}
const handleDeleteClass = () => {
if (!deletingClass) return
setScheduledClasses((prev) => prev.filter((c) => c.id !== 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)))
// 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',
@ -242,9 +225,7 @@ const ClassList: React.FC = () => {
</div>
<div className="ml-3 sm:ml-4">
<p className="text-xs sm:text-sm font-medium text-gray-600">Toplam Sınıf</p>
<p className="text-xl sm:text-2xl font-bold text-gray-900">
{scheduledClasses.length}
</p>
<p className="text-xl sm:text-2xl font-bold text-gray-900">{classList.length}</p>
</div>
</div>
</motion.div>
@ -262,7 +243,7 @@ const ClassList: React.FC = () => {
<div className="ml-3 sm:ml-4">
<p className="text-xs sm:text-sm font-medium text-gray-600">Aktif Sınıf</p>
<p className="text-xl sm:text-2xl font-bold text-gray-900">
{scheduledClasses.filter((c) => c.isActive).length}
{classList.filter((c) => c.isActive).length}
</p>
</div>
</div>
@ -281,7 +262,7 @@ const ClassList: React.FC = () => {
<div className="ml-3 sm:ml-4">
<p className="text-xs sm:text-sm font-medium text-gray-600">Toplam Katılımcı</p>
<p className="text-xl sm:text-2xl font-bold text-gray-900">
{scheduledClasses.reduce((sum, c) => sum + c.participantCount, 0)}
{classList.reduce((sum, c) => sum + c.participantCount, 0)}
</p>
</div>
</div>
@ -304,14 +285,14 @@ const ClassList: React.FC = () => {
)}
</div>
<div className="p-4 sm:p-6">
{scheduledClasses.length === 0 ? (
{classList.length === 0 ? (
<div className="text-center py-12">
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
<p className="text-gray-500">Henüz programlanmış sınıf bulunmamaktadır.</p>
</div>
) : (
<div className="grid gap-4 sm:gap-6">
{scheduledClasses.map((classSession, index) => (
{classList.map((classSession, index) => (
<motion.div
key={classSession.id}
initial={{ opacity: 0, x: -20 }}
@ -440,8 +421,8 @@ const ClassList: React.FC = () => {
</div>
</div>
{/* Create Class Modal */}
{showCreateModal && (
{/* Class Modal (Create/Edit) */}
{(showCreateModal || (showEditModal && editingClass)) && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
@ -449,18 +430,23 @@ const ClassList: React.FC = () => {
className="bg-white rounded-lg max-w-2xl w-full max-h-[95vh] overflow-y-auto"
>
<div className="p-3 sm:p-3 border-b border-gray-200">
<h2 className="text-xl sm:text-2xl font-bold text-gray-900">Yeni Sınıf Oluştur</h2>
<h2 className="text-xl sm:text-2xl font-bold text-gray-900">
{showCreateModal ? 'Yeni Sınıf Oluştur' : 'Sınıfı Düzenle'}
</h2>
</div>
<form onSubmit={handleCreateClass} className="p-4 sm:p-6 space-y-4 sm:space-y-6">
<form
onSubmit={showCreateModal ? handleCreateClass : handleEditClass}
className="p-4 sm:p-6 space-y-4 sm:space-y-6"
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Sınıf Adı *</label>
<input
type="text"
required
autoFocus
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
autoFocus={showCreateModal}
value={newClassroom.name}
onChange={(e) => setNewClassroom({ ...newClassroom, name: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Örn: Matematik 101 - Diferansiyel Denklemler"
/>
@ -469,8 +455,10 @@ const ClassList: React.FC = () => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">ıklama</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
value={newClassroom.description}
onChange={(e) =>
setNewClassroom({ ...newClassroom, description: e.target.value })
}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Ders hakkında kısa açıklama..."
@ -484,8 +472,8 @@ const ClassList: React.FC = () => {
</label>
<input
type="text"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
value={newClassroom.subject}
onChange={(e) => setNewClassroom({ ...newClassroom, subject: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Örn: Matematik, Fizik, Kimya"
/>
@ -498,10 +486,10 @@ const ClassList: React.FC = () => {
<input
type="datetime-local"
required
value={formData.scheduledStartTime}
value={newClassroom.scheduledStartTime}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
scheduledStartTime: e.target.value,
})
}
@ -519,10 +507,10 @@ const ClassList: React.FC = () => {
type="number"
min="15"
max="480"
value={formData.duration}
value={newClassroom.duration}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
duration: parseInt(e.target.value),
})
}
@ -538,10 +526,10 @@ const ClassList: React.FC = () => {
type="number"
min="1"
max="100"
value={formData.maxParticipants}
value={newClassroom.maxParticipants}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
maxParticipants: parseInt(e.target.value),
})
}
@ -560,12 +548,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.allowHandRaise}
checked={newClassroom.settings.allowHandRaise}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
settings: {
...formData.settings,
...newClassroom.settings,
allowHandRaise: e.target.checked,
},
})
@ -577,12 +565,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.allowStudentChat}
checked={newClassroom.settings.allowStudentChat}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
settings: {
...formData.settings,
...newClassroom.settings,
allowStudentChat: e.target.checked,
},
})
@ -594,12 +582,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.allowPrivateMessages}
checked={newClassroom.settings.allowPrivateMessages}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
settings: {
...formData.settings,
...newClassroom.settings,
allowPrivateMessages: e.target.checked,
},
})
@ -611,12 +599,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.allowStudentScreenShare}
checked={newClassroom.settings.allowStudentScreenShare}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
settings: {
...formData.settings,
...newClassroom.settings,
allowStudentScreenShare: e.target.checked,
},
})
@ -635,12 +623,12 @@ const ClassList: React.FC = () => {
Varsayılan mikrofon durumu
</label>
<select
value={formData.settings.defaultMicrophoneState}
value={newClassroom.settings.defaultMicrophoneState}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
settings: {
...formData.settings,
...newClassroom.settings,
defaultMicrophoneState: e.target.value as 'muted' | 'unmuted',
},
})
@ -657,12 +645,12 @@ const ClassList: React.FC = () => {
Varsayılan kamera durumu
</label>
<select
value={formData.settings.defaultCameraState}
value={newClassroom.settings.defaultCameraState}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
settings: {
...formData.settings,
...newClassroom.settings,
defaultCameraState: e.target.value as 'on' | 'off',
},
})
@ -679,12 +667,12 @@ const ClassList: React.FC = () => {
Varsayılan layout
</label>
<select
value={formData.settings.defaultLayout}
value={newClassroom.settings.defaultLayout}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
settings: {
...formData.settings,
...newClassroom.settings,
defaultLayout: e.target.value,
},
})
@ -701,12 +689,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.autoMuteNewParticipants}
checked={newClassroom.settings.autoMuteNewParticipants}
onChange={(e) =>
setFormData({
...formData,
setNewClassroom({
...newClassroom,
settings: {
...formData.settings,
...newClassroom.settings,
autoMuteNewParticipants: e.target.checked,
},
})
@ -719,142 +707,16 @@ const ClassList: React.FC = () => {
</div>
</div>
<div className="flex items-center justify-end space-x-4 pt-6 border-t border-gray-200">
<button
type="button"
onClick={() => setShowCreateModal(false)}
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
İptal
</button>
<button
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sınıf Oluştur
</button>
</div>
</form>
</motion.div>
</div>
)}
{/* Edit Class Modal */}
{showEditModal && editingClass && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-white rounded-lg max-w-2xl w-full max-h-[95vh] overflow-y-auto"
>
<div className="p-4 sm:p-6 border-b border-gray-200">
<h2 className="text-xl sm:text-2xl font-bold text-gray-900">Sınıfı Düzenle</h2>
</div>
<form onSubmit={handleEditClass} className="p-4 sm:p-6 space-y-4 sm:space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Sınıf Adı *</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">ıklama</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Ders hakkında kısa açıklama..."
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ders Konusu
</label>
<input
type="text"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: 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, Fizik, Kimya"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Başlangıç Tarihi ve Saati *
</label>
<input
type="datetime-local"
required
value={formData.scheduledStartTime}
onChange={(e) =>
setFormData({
...formData,
scheduledStartTime: 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"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Süre (dakika)
</label>
<input
type="number"
min="15"
max="480"
value={formData.duration}
onChange={(e) =>
setFormData({
...formData,
duration: parseInt(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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Maksimum Katılımcı
</label>
<input
type="number"
min="1"
max="100"
value={formData.maxParticipants}
onChange={(e) =>
setFormData({
...formData,
maxParticipants: parseInt(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"
/>
</div>
</div>
<div className="flex items-center justify-end space-x-4 pt-6 border-t border-gray-200">
<button
type="button"
onClick={() => {
setShowEditModal(false)
setEditingClass(null)
resetForm()
if (showCreateModal) setShowCreateModal(false)
if (showEditModal) {
setShowEditModal(false)
setEditingClass(null)
resetForm()
}
}}
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
@ -864,7 +726,7 @@ const ClassList: React.FC = () => {
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Değişiklikleri Kaydet
{showCreateModal ? 'Sınıf Oluştur' : 'Değişiklikleri Kaydet'}
</button>
</div>
</form>

View file

@ -1,29 +1,77 @@
import { useEffect } from 'react'
import React from 'react'
import { motion } from 'framer-motion'
import { FaGraduationCap, FaUserCheck, FaEye } from 'react-icons/fa'
import { Role } from '@/proxy/classroom/models'
import { useStoreActions, useStoreState } from '@/store/store'
import { useNavigate } from 'react-router-dom'
import { useClassroomLogic } from '@/utils/hooks/useClassroomLogic'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { Container } from '@/components/shared'
import { RoleSelector } from '@/components/classroom/RoleSelector'
import RoomDetail from './RoomDetail'
const Dashboard: React.FC = () => {
const { roleState, currentClass, handleRoleSelect, handleLeaveClass } = useClassroomLogic()
const navigate = useNavigate()
const { user } = useStoreState((state) => state.auth)
const { setUser } = useStoreActions((actions) => actions.auth.user)
// Eğer dashboard seçildiyse otomatik yönlendirme yap
useEffect(() => {
if (roleState === 'dashboard') {
navigate(ROUTES_ENUM.protected.admin.classroom.classes, { replace: true })
}
}, [roleState, navigate])
const handleRoleSelect = (role: Role) => {
setUser({
...user,
role,
})
navigate(ROUTES_ENUM.protected.admin.classroom.classes, { replace: true })
}
return (
<Container>
{roleState === 'role-selection' && <RoleSelector onRoleSelect={handleRoleSelect} />}
<div className="flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center w-full max-w-4xl"
>
<p className="text-lg sm:text-xl text-gray-600 mb-8 sm:mb-12">Lütfen rolünüzü seçin</p>
{roleState === 'classroom' && currentClass && <RoomDetail />}
</Container>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleRoleSelect('teacher')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-blue-500"
>
<FaGraduationCap size={48} className="mx-auto text-blue-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğretmen</h2>
<p className="text-gray-600 text-sm sm:text-base">
Ders başlatın, öğrencilerle iletişim kurun ve katılım raporlarını görün
</p>
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleRoleSelect('student')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-green-500"
>
<FaUserCheck size={48} className="mx-auto text-green-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğrenci</h2>
<p className="text-gray-600 text-sm sm:text-base">
Aktif derslere katılın, öğretmeniniz ve diğer öğrencilerle etkileşim kurun
</p>
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleRoleSelect('observer')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-purple-500 md:col-span-2 lg:col-span-1"
>
<FaEye size={48} className="mx-auto text-purple-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Gözlemci</h2>
<p className="text-gray-600 text-sm sm:text-base">
Sınıfı gözlemleyin, eğitim sürecini takip edin (ses/video paylaşımı yok)
</p>
</motion.button>
</div>
</motion.div>
</div>
)
}
export default Dashboard
export default Dashboard

View file

@ -69,13 +69,14 @@ const newClassSession: ClassroomDto = {
name: '',
teacherId: '',
teacherName: '',
startTime: '',
scheduledStartTime: '',
actualStartTime: '',
endTime: '',
isActive: false,
isScheduled: false,
participantCount: 0,
settings: undefined,
canJoin: false,
}
const RoomDetail: React.FC = () => {
@ -128,9 +129,7 @@ const RoomDetail: React.FC = () => {
allowStudentScreenShare: false,
allowStudentChat: true,
allowPrivateMessages: true,
autoMuteNewParticipants: true,
recordSession: false,
waitingRoomEnabled: false,
autoMuteNewParticipants: true
})
const signalRServiceRef = useRef<SignalRService>()
@ -1445,30 +1444,6 @@ const RoomDetail: React.FC = () => {
disabled={user.role !== 'teacher'}
/>
</label>
<label className="flex items-center justify-between">
<span className="text-sm">Bekleme odası aktif</span>
<input
type="checkbox"
checked={classSettings.waitingRoomEnabled}
onChange={(e) =>
handleSettingsChange({ waitingRoomEnabled: e.target.checked })
}
className="rounded"
disabled={user.role !== 'teacher'}
/>
</label>
<label className="flex items-center justify-between">
<span className="text-sm">Oturumu kaydet</span>
<input
type="checkbox"
checked={classSettings.recordSession}
onChange={(e) =>
handleSettingsChange({ recordSession: e.target.checked })
}
className="rounded"
disabled={user.role !== 'teacher'}
/>
</label>
</div>
</div>