Compare commits

...

2 commits

Author SHA1 Message Date
Sedat ÖZTÜRK
e4544ad1e7 Classroom backend ve ui 2025-08-26 11:39:09 +03:00
Sedat ÖZTÜRK
217c68b853 VirtualClass -> Classroom 2025-08-26 08:59:39 +03:00
45 changed files with 6149 additions and 621 deletions

View file

@ -1,8 +1,8 @@
using System;
namespace Kurs.Platform.VirtualClassrooms;
namespace Kurs.Platform.Classrooms;
public class ChatMessageDto
public class ClassChatDto
{
public Guid Id { get; set; }
public Guid SessionId { get; set; }

View file

@ -0,0 +1,16 @@
using System;
namespace Kurs.Platform.Classrooms;
public class ClassParticipantDto
{
public Guid Id { get; set; }
public Guid SessionId { get; set; }
public Guid UserId { get; set; }
public string UserName { get; set; }
public string UserEmail { get; set; }
public bool IsTeacher { get; set; }
public bool IsAudioMuted { get; set; }
public bool IsVideoMuted { get; set; }
public DateTime JoinTime { get; set; }
}

View file

@ -1,9 +1,9 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Kurs.Platform.VirtualClassrooms;
namespace Kurs.Platform.Classrooms;
public class ClassSessionDto : FullAuditedEntityDto<Guid>
public class ClassroomDto : FullAuditedEntityDto<Guid>
{
public string Name { get; set; }
public string Description { get; set; }
@ -21,7 +21,7 @@ public class ClassSessionDto : FullAuditedEntityDto<Guid>
public bool CanJoin { get; set; }
}
public class CreateClassSessionDto
public class CreateClassroomDto
{
public string Name { get; set; }
public string Description { get; set; }
@ -31,7 +31,7 @@ public class CreateClassSessionDto
public int MaxParticipants { get; set; } = 30;
}
public class UpdateClassSessionDto
public class UpdateClassroomDto
{
public string Name { get; set; }
public string Description { get; set; }
@ -41,13 +41,13 @@ public class UpdateClassSessionDto
public int MaxParticipants { get; set; }
}
public class GetClassSessionListDto : PagedAndSortedResultRequestDto
public class GetClassroomListDto : PagedAndSortedResultRequestDto
{
public bool? IsActive { get; set; }
public Guid? TeacherId { get; set; }
}
public class AttendanceRecordDto : EntityDto<Guid>
public class ClassAttendanceDto : EntityDto<Guid>
{
public Guid SessionId { get; set; }
public Guid StudentId { get; set; }

View file

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Kurs.Platform.Classrooms;
public interface IClassroomAppService : IApplicationService
{
Task<ClassroomDto> CreateAsync(CreateClassroomDto input);
Task<PagedResultDto<ClassroomDto>> GetListAsync(GetClassroomListDto input);
Task<ClassroomDto> GetAsync(Guid id);
Task<ClassroomDto> UpdateAsync(Guid id, UpdateClassroomDto input);
Task DeleteAsync(Guid id);
Task<ClassroomDto> StartClassAsync(Guid id);
Task EndClassAsync(Guid id);
Task<ClassroomDto> JoinClassAsync(Guid id);
Task LeaveClassAsync(Guid id);
Task<List<ClassAttendanceDto>> GetAttendanceAsync(Guid sessionId);
}

View file

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Kurs.Platform.VirtualClassrooms;
public interface IVirtualClassAppService : IApplicationService
{
Task<ClassSessionDto> CreateAsync(CreateClassSessionDto input);
Task<PagedResultDto<ClassSessionDto>> GetListAsync(GetClassSessionListDto input);
Task<ClassSessionDto> GetAsync(Guid id);
Task<ClassSessionDto> UpdateAsync(Guid id, UpdateClassSessionDto input);
Task DeleteAsync(Guid id);
Task<ClassSessionDto> StartClassAsync(Guid id);
Task EndClassAsync(Guid id);
Task<ClassSessionDto> JoinClassAsync(Guid id);
Task LeaveClassAsync(Guid id);
Task<List<AttendanceRecordDto>> GetAttendanceAsync(Guid sessionId);
}

View file

@ -1,16 +0,0 @@
using System;
namespace Kurs.Platform.VirtualClassrooms;
public class ParticipantDto
{
public Guid Id { get; set; }
public Guid SessionId { get; set; }
public Guid UserId { get; set; }
public string UserName { get; set; }
public string UserEmail { get; set; }
public bool IsTeacher { get; set; }
public bool IsAudioMuted { get; set; }
public bool IsVideoMuted { get; set; }
public DateTime JoinTime { get; set; }
}

View file

@ -7,28 +7,28 @@ using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
namespace Kurs.Platform.VirtualClassrooms;
namespace Kurs.Platform.Classrooms;
[Authorize]
public class VirtualClassAppService : PlatformAppService, IVirtualClassAppService
public class ClassroomAppService : PlatformAppService, IClassroomAppService
{
private readonly IRepository<ClassSession, Guid> _classSessionRepository;
private readonly IRepository<Participant, Guid> _participantRepository;
private readonly IRepository<AttendanceRecord, Guid> _attendanceRepository;
private readonly IRepository<Classroom, Guid> _classSessionRepository;
private readonly IRepository<ClassParticipant, Guid> _participantRepository;
private readonly IRepository<ClassAttandance, Guid> _attendanceRepository;
public VirtualClassAppService(
IRepository<ClassSession, Guid> classSessionRepository,
IRepository<Participant, Guid> participantRepository,
IRepository<AttendanceRecord, Guid> attendanceRepository)
public ClassroomAppService(
IRepository<Classroom, Guid> classSessionRepository,
IRepository<ClassParticipant, Guid> participantRepository,
IRepository<ClassAttandance, Guid> attendanceRepository)
{
_classSessionRepository = classSessionRepository;
_participantRepository = participantRepository;
_attendanceRepository = attendanceRepository;
}
public async Task<ClassSessionDto> CreateAsync(CreateClassSessionDto input)
public async Task<ClassroomDto> CreateAsync(CreateClassroomDto input)
{
var classSession = new ClassSession(
var classSession = new Classroom(
GuidGenerator.Create(),
input.Name,
input.Description,
@ -43,10 +43,10 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
await _classSessionRepository.InsertAsync(classSession);
await CurrentUnitOfWork.SaveChangesAsync();
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
return ObjectMapper.Map<Classroom, ClassroomDto>(classSession);
}
public async Task<PagedResultDto<ClassSessionDto>> GetListAsync(GetClassSessionListDto input)
public async Task<PagedResultDto<ClassroomDto>> GetListAsync(GetClassroomListDto input)
{
var query = await _classSessionRepository.GetQueryableAsync();
@ -67,19 +67,19 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
.Take(input.MaxResultCount)
.ToList();
return new PagedResultDto<ClassSessionDto>(
return new PagedResultDto<ClassroomDto>(
totalCount,
ObjectMapper.Map<List<ClassSession>, List<ClassSessionDto>>(items)
ObjectMapper.Map<List<Classroom>, List<ClassroomDto>>(items)
);
}
public async Task<ClassSessionDto> GetAsync(Guid id)
public async Task<ClassroomDto> GetAsync(Guid id)
{
var classSession = await _classSessionRepository.GetAsync(id);
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
return ObjectMapper.Map<Classroom, ClassroomDto>(classSession);
}
public async Task<ClassSessionDto> UpdateAsync(Guid id, UpdateClassSessionDto input)
public async Task<ClassroomDto> UpdateAsync(Guid id, UpdateClassroomDto input)
{
var classSession = await _classSessionRepository.GetAsync(id);
@ -101,7 +101,7 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
classSession.MaxParticipants = input.MaxParticipants;
await _classSessionRepository.UpdateAsync(classSession);
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
return ObjectMapper.Map<Classroom, ClassroomDto>(classSession);
}
public async Task DeleteAsync(Guid id)
@ -121,7 +121,7 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
await _classSessionRepository.DeleteAsync(id);
}
public async Task<ClassSessionDto> StartClassAsync(Guid id)
public async Task<ClassroomDto> StartClassAsync(Guid id)
{
var classSession = await _classSessionRepository.GetAsync(id);
@ -138,7 +138,7 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
classSession.StartClass();
await _classSessionRepository.UpdateAsync(classSession);
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
return ObjectMapper.Map<Classroom, ClassroomDto>(classSession);
}
public async Task EndClassAsync(Guid id)
@ -166,7 +166,7 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
}
}
public async Task<ClassSessionDto> JoinClassAsync(Guid id)
public async Task<ClassroomDto> JoinClassAsync(Guid id)
{
var classSession = await _classSessionRepository.GetAsync(id);
@ -188,7 +188,7 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
if (existingParticipant == null)
{
// Add participant
var participant = new Participant(
var participant = new ClassParticipant(
GuidGenerator.Create(),
id,
CurrentUser.Id,
@ -200,7 +200,7 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
await _participantRepository.InsertAsync(participant);
// Create attendance record
var attendance = new AttendanceRecord(
var attendance = new ClassAttandance(
GuidGenerator.Create(),
id,
CurrentUser.Id,
@ -215,7 +215,7 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
await _classSessionRepository.UpdateAsync(classSession);
}
return ObjectMapper.Map<ClassSession, ClassSessionDto>(classSession);
return ObjectMapper.Map<Classroom, ClassroomDto>(classSession);
}
public async Task LeaveClassAsync(Guid id)
@ -247,7 +247,7 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
}
}
public async Task<List<AttendanceRecordDto>> GetAttendanceAsync(Guid sessionId)
public async Task<List<ClassAttendanceDto>> GetAttendanceAsync(Guid sessionId)
{
var classSession = await _classSessionRepository.GetAsync(sessionId);
@ -260,6 +260,6 @@ public class VirtualClassAppService : PlatformAppService, IVirtualClassAppServic
x => x.SessionId == sessionId
);
return ObjectMapper.Map<List<AttendanceRecord>, List<AttendanceRecordDto>>(attendanceRecords);
return ObjectMapper.Map<List<ClassAttandance>, List<ClassAttendanceDto>>(attendanceRecords);
}
}

View file

@ -0,0 +1,20 @@
using AutoMapper;
using Kurs.Platform.Entities;
namespace Kurs.Platform.Classrooms;
public class ClassroomAutoMapperProfile : Profile
{
public ClassroomAutoMapperProfile()
{
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

@ -1,20 +0,0 @@
using AutoMapper;
using Kurs.Platform.Entities;
namespace Kurs.Platform.VirtualClassrooms;
public class VirtualClassAutoMapperProfile : Profile
{
public VirtualClassAutoMapperProfile()
{
CreateMap<ClassSession, ClassSessionDto>()
.ForMember(dest => dest.CanJoin, opt => opt.MapFrom(src => src.CanJoin()));
CreateMap<CreateClassSessionDto, ClassSession>();
CreateMap<UpdateClassSessionDto, ClassSession>();
CreateMap<AttendanceRecord, AttendanceRecordDto>();
CreateMap<Participant, ParticipantDto>();
CreateMap<ChatMessage, ChatMessageDto>();
}
}

View file

@ -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": [

View file

@ -3,7 +3,7 @@ using Volo.Abp.Domain.Entities.Auditing;
namespace Kurs.Platform.Entities;
public class AttendanceRecord : FullAuditedEntity<Guid>
public class ClassAttandance : FullAuditedEntity<Guid>
{
public Guid SessionId { get; set; }
public Guid? StudentId { get; set; }
@ -13,13 +13,13 @@ public class AttendanceRecord : FullAuditedEntity<Guid>
public int TotalDurationMinutes { get; set; }
// Navigation properties
public virtual ClassSession Session { get; set; }
public virtual Classroom Session { get; set; }
protected AttendanceRecord()
protected ClassAttandance()
{
}
public AttendanceRecord(
public ClassAttandance(
Guid id,
Guid sessionId,
Guid? studentId,

View file

@ -3,7 +3,7 @@ using Volo.Abp.Domain.Entities.Auditing;
namespace Kurs.Platform.Entities;
public class ChatMessage : FullAuditedEntity<Guid>
public class ClassChat : FullAuditedEntity<Guid>
{
public Guid SessionId { get; set; }
public Guid SenderId { get; set; }
@ -13,13 +13,13 @@ public class ChatMessage : FullAuditedEntity<Guid>
public bool IsTeacher { get; set; }
// Navigation properties
public virtual ClassSession Session { get; set; }
public virtual Classroom Session { get; set; }
protected ChatMessage()
protected ClassChat()
{
}
public ChatMessage(
public ClassChat(
Guid id,
Guid sessionId,
Guid senderId,

View file

@ -3,7 +3,7 @@ using Volo.Abp.Domain.Entities.Auditing;
namespace Kurs.Platform.Entities;
public class Participant : FullAuditedEntity<Guid>
public class ClassParticipant : FullAuditedEntity<Guid>
{
public Guid SessionId { get; set; }
public Guid? UserId { get; set; }
@ -16,13 +16,13 @@ public class Participant : FullAuditedEntity<Guid>
public string ConnectionId { get; set; }
// Navigation properties
public virtual ClassSession Session { get; set; }
public virtual Classroom Session { get; set; }
protected Participant()
protected ClassParticipant()
{
}
public Participant(
public ClassParticipant(
Guid id,
Guid sessionId,
Guid? userId,

View file

@ -4,7 +4,7 @@ using Volo.Abp.Domain.Entities.Auditing;
namespace Kurs.Platform.Entities;
public class ClassSession : FullAuditedEntity<Guid>
public class Classroom : FullAuditedEntity<Guid>
{
public string Name { get; set; }
public string Description { get; set; }
@ -20,18 +20,18 @@ public class ClassSession : FullAuditedEntity<Guid>
public bool IsScheduled { get; set; }
public int ParticipantCount { get; set; }
public virtual ICollection<Participant> Participants { get; set; }
public virtual ICollection<AttendanceRecord> AttendanceRecords { get; set; }
public virtual ICollection<ChatMessage> ChatMessages { get; set; }
public virtual ICollection<ClassParticipant> Participants { get; set; }
public virtual ICollection<ClassAttandance> AttendanceRecords { get; set; }
public virtual ICollection<ClassChat> ChatMessages { get; set; }
protected ClassSession()
protected Classroom()
{
Participants = new HashSet<Participant>();
AttendanceRecords = new HashSet<AttendanceRecord>();
ChatMessages = new HashSet<ChatMessage>();
Participants = new HashSet<ClassParticipant>();
AttendanceRecords = new HashSet<ClassAttandance>();
ChatMessages = new HashSet<ClassChat>();
}
public ClassSession(
public Classroom(
Guid id,
string name,
string description,
@ -55,9 +55,9 @@ public class ClassSession : FullAuditedEntity<Guid>
IsScheduled = true;
ParticipantCount = 0;
Participants = new HashSet<Participant>();
AttendanceRecords = new HashSet<AttendanceRecord>();
ChatMessages = new HashSet<ChatMessage>();
Participants = new HashSet<ClassParticipant>();
AttendanceRecords = new HashSet<ClassAttandance>();
ChatMessages = new HashSet<ClassChat>();
}
public void StartClass()

View file

@ -97,10 +97,10 @@ public class PlatformDbContext :
public DbSet<Demo> Demos { get; set; }
public DbSet<Service> Services { get; set; }
public DbSet<ClassSession> ClassSessions { get; set; }
public DbSet<Participant> Participants { get; set; }
public DbSet<AttendanceRecord> AttendanceRecords { get; set; }
public DbSet<ChatMessage> ChatMessages { get; set; }
public DbSet<Classroom> ClassSessions { get; set; }
public DbSet<ClassParticipant> Participants { get; set; }
public DbSet<ClassAttandance> AttendanceRecords { get; set; }
public DbSet<ClassChat> ChatMessages { get; set; }
#region Entities from the modules
@ -882,9 +882,9 @@ public class PlatformDbContext :
});
// ClassSession
builder.Entity<ClassSession>(b =>
builder.Entity<Classroom>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassSession), PlatformConsts.DbSchema);
b.ToTable(PlatformConsts.DbTablePrefix + nameof(Classroom), PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(200);
@ -914,9 +914,9 @@ public class PlatformDbContext :
});
// Participant
builder.Entity<Participant>(b =>
builder.Entity<ClassParticipant>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + nameof(Participant), PlatformConsts.DbSchema);
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassParticipant), PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.UserName).IsRequired().HasMaxLength(100);
@ -929,9 +929,9 @@ public class PlatformDbContext :
});
// AttendanceRecord
builder.Entity<AttendanceRecord>(b =>
builder.Entity<ClassAttandance>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + nameof(AttendanceRecord), PlatformConsts.DbSchema);
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassAttandance), PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.StudentName).IsRequired().HasMaxLength(100);
@ -942,9 +942,9 @@ public class PlatformDbContext :
});
// ChatMessage
builder.Entity<ChatMessage>(b =>
builder.Entity<ClassChat>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ChatMessage), PlatformConsts.DbSchema);
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassChat), PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.SenderName).IsRequired().HasMaxLength(100);

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20250826054655_Initial")]
[Migration("20250826055306_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -852,72 +852,6 @@ namespace Kurs.Platform.Migrations
b.ToTable("PApiMigration", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.AttendanceRecord", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime>("JoinTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTime?>("LeaveTime")
.HasColumnType("datetime2");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("StudentId")
.HasColumnType("uniqueidentifier");
b.Property<string>("StudentName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("TotalDurationMinutes")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JoinTime");
b.HasIndex("SessionId");
b.HasIndex("StudentId");
b.ToTable("PAttendanceRecord", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.BackgroundWorker", b =>
{
b.Property<Guid>("Id")
@ -1563,7 +1497,135 @@ namespace Kurs.Platform.Migrations
b.ToTable("PChart", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ChatMessage", b =>
modelBuilder.Entity("Kurs.Platform.Entities.City", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("nvarchar(16)");
b.Property<string>("CountryCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("PlateCode")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.HasKey("Id");
b.HasIndex("CountryCode", "Code")
.IsUnique();
b.ToTable("PCity", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime>("JoinTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTime?>("LeaveTime")
.HasColumnType("datetime2");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("StudentId")
.HasColumnType("uniqueidentifier");
b.Property<string>("StudentName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("TotalDurationMinutes")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JoinTime");
b.HasIndex("SessionId");
b.HasIndex("StudentId");
b.ToTable("PClassAttandance", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
@ -1628,23 +1690,17 @@ namespace Kurs.Platform.Migrations
b.HasIndex("Timestamp");
b.ToTable("PChatMessage", (string)null);
b.ToTable("PClassChat", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.City", b =>
modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("nvarchar(16)");
b.Property<string>("CountryCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)");
b.Property<string>("ConnectionId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
@ -1662,12 +1718,24 @@ namespace Kurs.Platform.Migrations
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsAudioMuted")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsTeacher")
.HasColumnType("bit");
b.Property<bool>("IsVideoMuted")
.HasColumnType("bit");
b.Property<DateTime>("JoinTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
@ -1676,24 +1744,35 @@ namespace Kurs.Platform.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<string>("PlateCode")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("UserEmail")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<Guid?>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Id");
b.HasIndex("CountryCode", "Code")
.IsUnique();
b.HasIndex("SessionId");
b.ToTable("PCity", (string)null);
b.HasIndex("UserId");
b.HasIndex("SessionId", "UserId")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("PClassParticipant", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassSession", b =>
modelBuilder.Entity("Kurs.Platform.Entities.Classroom", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
@ -1781,7 +1860,7 @@ namespace Kurs.Platform.Migrations
b.HasIndex("TeacherId");
b.ToTable("PClassSession", (string)null);
b.ToTable("PClassroom", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Contact", b =>
@ -3277,85 +3356,6 @@ namespace Kurs.Platform.Migrations
b.ToTable("PMenu", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Participant", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("ConnectionId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsAudioMuted")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsTeacher")
.HasColumnType("bit");
b.Property<bool>("IsVideoMuted")
.HasColumnType("bit");
b.Property<DateTime>("JoinTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserEmail")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<Guid?>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Id");
b.HasIndex("SessionId");
b.HasIndex("UserId");
b.HasIndex("SessionId", "UserId")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("PParticipant", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.PaymentMethod", b =>
{
b.Property<string>("Id")
@ -6520,17 +6520,6 @@ namespace Kurs.Platform.Migrations
b.Navigation("Entity");
});
modelBuilder.Entity("Kurs.Platform.Entities.AttendanceRecord", b =>
{
b.HasOne("Kurs.Platform.Entities.ClassSession", "Session")
.WithMany("AttendanceRecords")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.BankAccount", b =>
{
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
@ -6559,17 +6548,6 @@ namespace Kurs.Platform.Migrations
b.Navigation("Category");
});
modelBuilder.Entity("Kurs.Platform.Entities.ChatMessage", b =>
{
b.HasOne("Kurs.Platform.Entities.ClassSession", "Session")
.WithMany("ChatMessages")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.City", b =>
{
b.HasOne("Kurs.Platform.Entities.Country", "Country")
@ -6582,6 +6560,39 @@ namespace Kurs.Platform.Migrations
b.Navigation("Country");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("AttendanceRecords")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("ChatMessages")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("Participants")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
{
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
@ -6633,17 +6644,6 @@ namespace Kurs.Platform.Migrations
.IsRequired();
});
modelBuilder.Entity("Kurs.Platform.Entities.Participant", b =>
{
b.HasOne("Kurs.Platform.Entities.ClassSession", "Session")
.WithMany("Participants")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ReportGenerated", b =>
{
b.HasOne("Kurs.Platform.Entities.ReportTemplate", "Template")
@ -6905,7 +6905,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Districts");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassSession", b =>
modelBuilder.Entity("Kurs.Platform.Entities.Classroom", b =>
{
b.Navigation("AttendanceRecords");

View file

@ -787,7 +787,7 @@ namespace Kurs.Platform.Migrations
});
migrationBuilder.CreateTable(
name: "PClassSession",
name: "PClassroom",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -814,7 +814,7 @@ namespace Kurs.Platform.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_PClassSession", x => x.Id);
table.PrimaryKey("PK_PClassroom", x => x.Id);
});
migrationBuilder.CreateTable(
@ -1922,7 +1922,7 @@ namespace Kurs.Platform.Migrations
});
migrationBuilder.CreateTable(
name: "PAttendanceRecord",
name: "PClassAttandance",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -1942,17 +1942,17 @@ namespace Kurs.Platform.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_PAttendanceRecord", x => x.Id);
table.PrimaryKey("PK_PClassAttandance", x => x.Id);
table.ForeignKey(
name: "FK_PAttendanceRecord_PClassSession_SessionId",
name: "FK_PClassAttandance_PClassroom_SessionId",
column: x => x.SessionId,
principalTable: "PClassSession",
principalTable: "PClassroom",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PChatMessage",
name: "PClassChat",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -1972,17 +1972,17 @@ namespace Kurs.Platform.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_PChatMessage", x => x.Id);
table.PrimaryKey("PK_PClassChat", x => x.Id);
table.ForeignKey(
name: "FK_PChatMessage_PClassSession_SessionId",
name: "FK_PClassChat_PClassroom_SessionId",
column: x => x.SessionId,
principalTable: "PClassSession",
principalTable: "PClassroom",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PParticipant",
name: "PClassParticipant",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -2005,11 +2005,11 @@ namespace Kurs.Platform.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_PParticipant", x => x.Id);
table.PrimaryKey("PK_PClassParticipant", x => x.Id);
table.ForeignKey(
name: "FK_PParticipant_PClassSession_SessionId",
name: "FK_PClassParticipant_PClassroom_SessionId",
column: x => x.SessionId,
principalTable: "PClassSession",
principalTable: "PClassroom",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
@ -2954,21 +2954,6 @@ namespace Kurs.Platform.Migrations
table: "PApiMigration",
column: "EntityId");
migrationBuilder.CreateIndex(
name: "IX_PAttendanceRecord_JoinTime",
table: "PAttendanceRecord",
column: "JoinTime");
migrationBuilder.CreateIndex(
name: "IX_PAttendanceRecord_SessionId",
table: "PAttendanceRecord",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_PAttendanceRecord_StudentId",
table: "PAttendanceRecord",
column: "StudentId");
migrationBuilder.CreateIndex(
name: "IX_MailQueueTableFormat",
table: "PBackgroundWorker_MailQueueTableFormat",
@ -3000,21 +2985,6 @@ namespace Kurs.Platform.Migrations
table: "PBlogPost",
column: "Slug");
migrationBuilder.CreateIndex(
name: "IX_PChatMessage_SenderId",
table: "PChatMessage",
column: "SenderId");
migrationBuilder.CreateIndex(
name: "IX_PChatMessage_SessionId",
table: "PChatMessage",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_PChatMessage_Timestamp",
table: "PChatMessage",
column: "Timestamp");
migrationBuilder.CreateIndex(
name: "IX_PCity_CountryCode_Code",
table: "PCity",
@ -3022,18 +2992,65 @@ namespace Kurs.Platform.Migrations
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PClassSession_IsActive",
table: "PClassSession",
name: "IX_PClassAttandance_JoinTime",
table: "PClassAttandance",
column: "JoinTime");
migrationBuilder.CreateIndex(
name: "IX_PClassAttandance_SessionId",
table: "PClassAttandance",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_PClassAttandance_StudentId",
table: "PClassAttandance",
column: "StudentId");
migrationBuilder.CreateIndex(
name: "IX_PClassChat_SenderId",
table: "PClassChat",
column: "SenderId");
migrationBuilder.CreateIndex(
name: "IX_PClassChat_SessionId",
table: "PClassChat",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_PClassChat_Timestamp",
table: "PClassChat",
column: "Timestamp");
migrationBuilder.CreateIndex(
name: "IX_PClassParticipant_SessionId",
table: "PClassParticipant",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_PClassParticipant_SessionId_UserId",
table: "PClassParticipant",
columns: new[] { "SessionId", "UserId" },
unique: true,
filter: "[UserId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_PClassParticipant_UserId",
table: "PClassParticipant",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_PClassroom_IsActive",
table: "PClassroom",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_PClassSession_ScheduledStartTime",
table: "PClassSession",
name: "IX_PClassroom_ScheduledStartTime",
table: "PClassroom",
column: "ScheduledStartTime");
migrationBuilder.CreateIndex(
name: "IX_PClassSession_TeacherId",
table: "PClassSession",
name: "IX_PClassroom_TeacherId",
table: "PClassroom",
column: "TeacherId");
migrationBuilder.CreateIndex(
@ -3115,23 +3132,6 @@ namespace Kurs.Platform.Migrations
table: "POrderItem",
column: "OrderId");
migrationBuilder.CreateIndex(
name: "IX_PParticipant_SessionId",
table: "PParticipant",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_PParticipant_SessionId_UserId",
table: "PParticipant",
columns: new[] { "SessionId", "UserId" },
unique: true,
filter: "[UserId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_PParticipant_UserId",
table: "PParticipant",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_PReportCategory_Name",
table: "PReportCategory",
@ -3283,9 +3283,6 @@ namespace Kurs.Platform.Migrations
migrationBuilder.DropTable(
name: "PApiMigration");
migrationBuilder.DropTable(
name: "PAttendanceRecord");
migrationBuilder.DropTable(
name: "PBackgroundWorker");
@ -3311,7 +3308,13 @@ namespace Kurs.Platform.Migrations
name: "PChart");
migrationBuilder.DropTable(
name: "PChatMessage");
name: "PClassAttandance");
migrationBuilder.DropTable(
name: "PClassChat");
migrationBuilder.DropTable(
name: "PClassParticipant");
migrationBuilder.DropTable(
name: "PContact");
@ -3382,9 +3385,6 @@ namespace Kurs.Platform.Migrations
migrationBuilder.DropTable(
name: "POrderItem");
migrationBuilder.DropTable(
name: "PParticipant");
migrationBuilder.DropTable(
name: "PPaymentMethod");
@ -3445,6 +3445,9 @@ namespace Kurs.Platform.Migrations
migrationBuilder.DropTable(
name: "PBlogCategory");
migrationBuilder.DropTable(
name: "PClassroom");
migrationBuilder.DropTable(
name: "PCustomEntity");
@ -3463,9 +3466,6 @@ namespace Kurs.Platform.Migrations
migrationBuilder.DropTable(
name: "POrder");
migrationBuilder.DropTable(
name: "PClassSession");
migrationBuilder.DropTable(
name: "PReportTemplate");

View file

@ -849,72 +849,6 @@ namespace Kurs.Platform.Migrations
b.ToTable("PApiMigration", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.AttendanceRecord", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime>("JoinTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTime?>("LeaveTime")
.HasColumnType("datetime2");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("StudentId")
.HasColumnType("uniqueidentifier");
b.Property<string>("StudentName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("TotalDurationMinutes")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JoinTime");
b.HasIndex("SessionId");
b.HasIndex("StudentId");
b.ToTable("PAttendanceRecord", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.BackgroundWorker", b =>
{
b.Property<Guid>("Id")
@ -1560,7 +1494,135 @@ namespace Kurs.Platform.Migrations
b.ToTable("PChart", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ChatMessage", b =>
modelBuilder.Entity("Kurs.Platform.Entities.City", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("nvarchar(16)");
b.Property<string>("CountryCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("PlateCode")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.HasKey("Id");
b.HasIndex("CountryCode", "Code")
.IsUnique();
b.ToTable("PCity", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime>("JoinTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTime?>("LeaveTime")
.HasColumnType("datetime2");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("StudentId")
.HasColumnType("uniqueidentifier");
b.Property<string>("StudentName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("TotalDurationMinutes")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JoinTime");
b.HasIndex("SessionId");
b.HasIndex("StudentId");
b.ToTable("PClassAttandance", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
@ -1625,23 +1687,17 @@ namespace Kurs.Platform.Migrations
b.HasIndex("Timestamp");
b.ToTable("PChatMessage", (string)null);
b.ToTable("PClassChat", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.City", b =>
modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("nvarchar(16)");
b.Property<string>("CountryCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)");
b.Property<string>("ConnectionId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
@ -1659,12 +1715,24 @@ namespace Kurs.Platform.Migrations
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsAudioMuted")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsTeacher")
.HasColumnType("bit");
b.Property<bool>("IsVideoMuted")
.HasColumnType("bit");
b.Property<DateTime>("JoinTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
@ -1673,24 +1741,35 @@ namespace Kurs.Platform.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<string>("PlateCode")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("UserEmail")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<Guid?>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Id");
b.HasIndex("CountryCode", "Code")
.IsUnique();
b.HasIndex("SessionId");
b.ToTable("PCity", (string)null);
b.HasIndex("UserId");
b.HasIndex("SessionId", "UserId")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("PClassParticipant", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassSession", b =>
modelBuilder.Entity("Kurs.Platform.Entities.Classroom", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
@ -1778,7 +1857,7 @@ namespace Kurs.Platform.Migrations
b.HasIndex("TeacherId");
b.ToTable("PClassSession", (string)null);
b.ToTable("PClassroom", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Contact", b =>
@ -3274,85 +3353,6 @@ namespace Kurs.Platform.Migrations
b.ToTable("PMenu", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Participant", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("ConnectionId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsAudioMuted")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsTeacher")
.HasColumnType("bit");
b.Property<bool>("IsVideoMuted")
.HasColumnType("bit");
b.Property<DateTime>("JoinTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserEmail")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<Guid?>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Id");
b.HasIndex("SessionId");
b.HasIndex("UserId");
b.HasIndex("SessionId", "UserId")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("PParticipant", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.PaymentMethod", b =>
{
b.Property<string>("Id")
@ -6517,17 +6517,6 @@ namespace Kurs.Platform.Migrations
b.Navigation("Entity");
});
modelBuilder.Entity("Kurs.Platform.Entities.AttendanceRecord", b =>
{
b.HasOne("Kurs.Platform.Entities.ClassSession", "Session")
.WithMany("AttendanceRecords")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.BankAccount", b =>
{
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
@ -6556,17 +6545,6 @@ namespace Kurs.Platform.Migrations
b.Navigation("Category");
});
modelBuilder.Entity("Kurs.Platform.Entities.ChatMessage", b =>
{
b.HasOne("Kurs.Platform.Entities.ClassSession", "Session")
.WithMany("ChatMessages")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.City", b =>
{
b.HasOne("Kurs.Platform.Entities.Country", "Country")
@ -6579,6 +6557,39 @@ namespace Kurs.Platform.Migrations
b.Navigation("Country");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("AttendanceRecords")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("ChatMessages")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("Participants")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
{
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
@ -6630,17 +6641,6 @@ namespace Kurs.Platform.Migrations
.IsRequired();
});
modelBuilder.Entity("Kurs.Platform.Entities.Participant", b =>
{
b.HasOne("Kurs.Platform.Entities.ClassSession", "Session")
.WithMany("Participants")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ReportGenerated", b =>
{
b.HasOne("Kurs.Platform.Entities.ReportTemplate", "Template")
@ -6902,7 +6902,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Districts");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassSession", b =>
modelBuilder.Entity("Kurs.Platform.Entities.Classroom", b =>
{
b.Navigation("AttendanceRecords");

View file

@ -12,16 +12,16 @@ namespace Kurs.Platform.SignalR.Hubs;
[Authorize]
public class ClassroomHub : Hub
{
private readonly IRepository<ClassSession, Guid> _classSessionRepository;
private readonly IRepository<Participant, Guid> _participantRepository;
private readonly IRepository<ChatMessage, Guid> _chatMessageRepository;
private readonly IRepository<Classroom, Guid> _classSessionRepository;
private readonly IRepository<ClassParticipant, Guid> _participantRepository;
private readonly IRepository<ClassChat, Guid> _chatMessageRepository;
private readonly ILogger<ClassroomHub> _logger;
private readonly IGuidGenerator _guidGenerator;
public ClassroomHub(
IRepository<ClassSession, Guid> classSessionRepository,
IRepository<Participant, Guid> participantRepository,
IRepository<ChatMessage, Guid> chatMessageRepository,
IRepository<Classroom, Guid> classSessionRepository,
IRepository<ClassParticipant, Guid> participantRepository,
IRepository<ClassChat, Guid> chatMessageRepository,
ILogger<ClassroomHub> logger,
IGuidGenerator guidGenerator)
{
@ -91,7 +91,7 @@ public class ClassroomHub : Hub
var isTeacher = participant?.IsTeacher ?? false;
// Save message to database
var chatMessage = new ChatMessage(
var chatMessage = new ClassChat(
_guidGenerator.Create(),
sessionId,
userId,

View file

@ -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"), {

182
ui/package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<ClassroomDto>) => void
onJoinClass: (classSession: ClassroomDto) => void
onEditClass: (classId: string, classData: Partial<ClassroomDto>) => void
onDeleteClass: (classId: string) => void
}
export const ClassList: React.FC<DashboardProps> = ({
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<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({
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<ClassroomDto> = {
...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 (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* Header */}
<div className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">
Sanal Sınıf Dashboard
</h1>
<p className="text-gray-600">Hoş geldiniz, {user.name}</p>
</div>
{user.role === 'teacher' && (
<button
onClick={() => setShowCreateModal(true)}
className="flex items-center justify-center space-x-2 bg-blue-600 text-white px-4 sm:px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors w-full sm:w-auto"
>
<FaPlus size={20} />
<span className="hidden sm:inline">Yeni Sınıf Oluştur</span>
<span className="sm:hidden">Yeni Sınıf</span>
</button>
)}
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 mb-6 sm:mb-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
>
<div className="flex items-center">
<div className="p-2 sm:p-3 bg-blue-100 rounded-full">
<FaCalendarAlt className="text-blue-600" size={20} />
</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>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
>
<div className="flex items-center">
<div className="p-2 sm:p-3 bg-green-100 rounded-full">
<FaPlay className="text-green-600" size={20} />
</div>
<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}
</p>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-white rounded-lg shadow-md p-4 sm:p-6 sm:col-span-2 lg:col-span-1"
>
<div className="flex items-center">
<div className="p-2 sm:p-3 bg-purple-100 rounded-full">
<FaUsers className="text-purple-600" size={20} />
</div>
<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)}
</p>
</div>
</div>
</motion.div>
</div>
{/* Scheduled Classes */}
<div className="bg-white rounded-lg shadow-md">
<div className="px-4 sm:px-6 py-4 border-b border-gray-200">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900">Programlı Sınıflar</h2>
</div>
<div className="p-4 sm:p-6">
{scheduledClasses.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) => (
<motion.div
key={classSession.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:shadow-md transition-shadow"
>
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between space-y-4 lg:space-y-0">
<div className="flex-1">
<div className="flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0 sm:space-x-3 mb-2">
<h3 className="text-base sm:text-lg font-semibold text-gray-900 break-words">
{classSession.name}
</h3>
<span
className={`px-2 py-1 rounded-full text-xs font-medium self-start ${
classSession.isActive
? 'bg-green-100 text-green-800'
: canJoinClass(classSession.scheduledStartTime)
? 'bg-yellow-100 text-yellow-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{classSession.isActive
? 'Aktif'
: canJoinClass(classSession.scheduledStartTime)
? 'Katılım Açık'
: 'Beklemede'}
</span>
</div>
<p className="text-gray-600 mb-3 text-sm sm:text-base">
{classSession.description}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-4 text-xs sm:text-sm text-gray-600">
<div className="flex items-center space-x-2">
<FaCalendarAlt size={12} className="flex-shrink-0" />
<span className="truncate">
{formatDateTime(classSession.scheduledStartTime)}
</span>
</div>
<div className="flex items-center space-x-2">
<FaClock size={12} className="flex-shrink-0" />
<span>{classSession.duration} dakika</span>
</div>
<div className="flex items-center space-x-2">
<FaUsers size={12} className="flex-shrink-0" />
<span>
{classSession.participantCount}/{classSession.maxParticipants}
</span>
</div>
<div className="flex items-center space-x-2">
<FaEye size={12} className="flex-shrink-0" />
<span className="truncate">
{getTimeUntilClass(classSession.scheduledStartTime)}
</span>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center space-y-2 sm:space-y-0 sm:space-x-2 lg:ml-4 w-full lg:w-auto">
{user.role === 'teacher' && classSession.teacherId === user.id && (
<div className="flex space-x-2">
<button
onClick={() => openEditModal(classSession)}
disabled={classSession.isActive}
className="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
title="Sınıfı Düzenle"
>
<FaEdit size={14} />
</button>
<button
onClick={() => openDeleteModal(classSession)}
disabled={classSession.isActive}
className="p-2 text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
title="Sınıfı Sil"
>
<FaTrash size={14} />
</button>
</div>
)}
{canJoinClass(classSession.scheduledStartTime) && (
<button
onClick={() =>
user.role === 'teacher' && classSession.teacherId === user.id
? handleStartClass(classSession)
: (() => {
onJoinClass(classSession)
if (classSession.id)
navigate(
ROUTES_ENUM.protected.admin.classroom.classroom.replace(
':id',
classSession.id,
),
)
})()
}
className={`px-3 sm:px-4 py-2 rounded-lg font-medium transition-colors text-sm sm:text-base w-full sm:w-auto ${
user.role === 'teacher' && classSession.teacherId === user.id
? 'bg-green-600 text-white hover:bg-green-700'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{user.role === 'teacher' && classSession.teacherId === user.id
? classSession.isActive
? 'Sınıfa Git'
: 'Dersi Başlat'
: 'Katıl'}
</button>
)}
</div>
</div>
</motion.div>
))}
</div>
)}
</div>
</div>
</div>
{/* Create Class Modal */}
{showCreateModal && (
<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">Yeni Sınıf Oluştur</h2>
</div>
<form onSubmit={handleCreateClass} 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>
{/* Sınıf Ayarları */}
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4">Sınıf Ayarları</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
<div className="space-y-4">
<h4 className="font-medium text-gray-700">Katılımcı İzinleri</h4>{' '}
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.allowHandRaise}
onChange={(e) =>
setFormData({
...formData,
settings: {
...formData.settings,
allowHandRaise: e.target.checked,
},
})
}
className="rounded"
/>
<span className="text-sm">Parmak kaldırma izni</span>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.allowStudentChat}
onChange={(e) =>
setFormData({
...formData,
settings: {
...formData.settings,
allowStudentChat: e.target.checked,
},
})
}
className="rounded"
/>
<span className="text-sm">Öğrenci sohbet izni</span>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.allowPrivateMessages}
onChange={(e) =>
setFormData({
...formData,
settings: {
...formData.settings,
allowPrivateMessages: e.target.checked,
},
})
}
className="rounded"
/>
<span className="text-sm">Özel mesaj izni</span>
</label>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.allowStudentScreenShare}
onChange={(e) =>
setFormData({
...formData,
settings: {
...formData.settings,
allowStudentScreenShare: e.target.checked,
},
})
}
className="rounded"
/>
<span className="text-sm">Öğrenci ekran paylaşımı</span>
</label>
</div>
<div className="space-y-4">
<h4 className="font-medium text-gray-700">Varsayılan Ayarlar</h4>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Varsayılan mikrofon durumu
</label>
<select
value={formData.settings.defaultMicrophoneState}
onChange={(e) =>
setFormData({
...formData,
settings: {
...formData.settings,
defaultMicrophoneState: e.target.value as 'muted' | 'unmuted',
},
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="muted">Kapalı</option>
<option value="unmuted">ık</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Varsayılan kamera durumu
</label>
<select
value={formData.settings.defaultCameraState}
onChange={(e) =>
setFormData({
...formData,
settings: {
...formData.settings,
defaultCameraState: e.target.value as 'on' | 'off',
},
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="on">ık</option>
<option value="off">Kapalı</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Varsayılan layout
</label>
<select
value={formData.settings.defaultLayout}
onChange={(e) =>
setFormData({
...formData,
settings: {
...formData.settings,
defaultLayout: 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"
>
<option value="grid">Izgara Görünümü</option>
<option value="teacher-focus">Öğretmen Odaklı</option>
<option value="presentation">Sunum Modu</option>
<option value="sidebar">Yan Panel</option>
</select>
</div>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={formData.settings.autoMuteNewParticipants}
onChange={(e) =>
setFormData({
...formData,
settings: {
...formData.settings,
autoMuteNewParticipants: e.target.checked,
},
})
}
className="rounded"
/>
<span className="text-sm">Yeni katılımcıları otomatik sustur</span>
</label>
</div>
</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()
}}
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"
>
Değişiklikleri Kaydet
</button>
</div>
</form>
</motion.div>
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && deletingClass && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-white rounded-lg max-w-md w-full mx-4"
>
<div className="p-6">
<div className="flex items-center mb-4">
<div className="p-3 bg-red-100 rounded-full mr-4">
<FaTrash className="text-red-600" size={24} />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">Sınıfı Sil</h3>
<p className="text-sm text-gray-600">Bu işlem geri alınamaz</p>
</div>
</div>
<p className="text-gray-700 mb-6">
<strong>"{deletingClass.name}"</strong> adlı sınıfı silmek istediğinizden emin
misiniz?
</p>
<div className="flex items-center justify-end space-x-4">
<button
onClick={() => {
setShowDeleteModal(false)
setDeletingClass(null)
}}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
İptal
</button>
<button
onClick={handleDeleteClass}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
>
Sil
</button>
</div>
</div>
</motion.div>
</div>
)}
</div>
)
}

View file

@ -0,0 +1,38 @@
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { RoleSelector } from './RoleSelector'
import { useClassroomLogic } from '@/utils/hooks/useClassroomLogic'
import { useStoreState } from '@/store/store'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { Room } from './Room'
export function Dashboard() {
const {
appState,
currentClass,
handleRoleSelect,
handleJoinClass,
handleLeaveClass,
handleCreateClass,
handleEditClass,
handleDeleteClass,
} = useClassroomLogic()
const navigate = useNavigate()
const { user } = useStoreState((state) => state.auth)
useEffect(() => {
if (appState === 'dashboard') {
navigate(ROUTES_ENUM.protected.admin.classroom.classes, { replace: true })
}
}, [appState, navigate])
if (appState === 'role-selection') {
return <RoleSelector onRoleSelect={handleRoleSelect} />
} else if (appState === 'dashboard') {
// Yönlendirme yapılacağı için burada içerik render etmiyoruz
return null
} else if (appState === 'classroom' && currentClass) {
return <Room classSession={currentClass} onLeaveClass={handleLeaveClass} />
}
return null
}

View file

@ -0,0 +1,81 @@
import React from 'react';
import { motion } from 'framer-motion';
import { FaUserTimes, FaExclamationTriangle } from 'react-icons/fa';
interface KickParticipantModalProps {
participant: { id: string; name: string } | null;
isOpen: boolean;
onClose: () => void;
onConfirm: (participantId: string) => void;
}
export const KickParticipantModal: React.FC<KickParticipantModalProps> = ({
participant,
isOpen,
onClose,
onConfirm,
}) => {
if (!isOpen || !participant) return null;
const handleConfirm = () => {
onConfirm(participant.id);
onClose();
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-white rounded-lg max-w-md w-full mx-4"
>
<div className="p-6">
<div className="flex items-center mb-4">
<div className="p-3 bg-red-100 rounded-full mr-4">
<FaExclamationTriangle className="text-red-600" size={24} />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">Katılımcıyı Çıkar</h3>
<p className="text-sm text-gray-600">Bu işlem geri alınamaz</p>
</div>
</div>
<div className="mb-6">
<p className="text-gray-700 mb-2">
<strong>"{participant.name}"</strong> adlı katılımcıyı sınıftan çıkarmak istediğinizden emin misiniz?
</p>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<div className="flex items-start">
<FaExclamationTriangle className="text-yellow-600 mt-0.5 mr-2" size={16} />
<div className="text-sm text-yellow-800">
<p className="font-medium">Dikkat:</p>
<ul className="mt-1 list-disc list-inside space-y-1">
<li>Katılımcı anında sınıftan çıkarılacak</li>
<li>Tekrar katılım için davet gerekebilir</li>
<li>Katılım süresi kaydedilecek</li>
</ul>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-end space-x-4">
<button
onClick={onClose}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
İptal
</button>
<button
onClick={handleConfirm}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center space-x-2"
>
<FaUserTimes size={16} />
<span>Çıkar</span>
</button>
</div>
</div>
</motion.div>
</div>
);
};

View file

@ -0,0 +1,118 @@
import { ClassAttendanceDto } from '@/proxy/classroom/models'
import React, { useEffect, useState } from 'react'
import { FaClock, FaUsers } from 'react-icons/fa'
interface AttendancePanelProps {
attendanceRecords: ClassAttendanceDto[]
isOpen: boolean
onClose: () => void
}
export const AttendancePanel: React.FC<AttendancePanelProps> = ({
attendanceRecords,
isOpen,
onClose,
}) => {
// Anlık süre güncellemesi için state ve timer
const [now, setNow] = useState(Date.now())
useEffect(() => {
if (!isOpen) return
const interval = setInterval(() => setNow(Date.now()), 60000) // her dakika
return () => clearInterval(interval)
}, [isOpen])
const formatDuration = (minutes: number) => {
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
if (hours > 0) {
return `${hours}h ${mins}m`
}
return `${mins}m`
}
const formatTime = (timeString: string) => {
return new Date(timeString).toLocaleTimeString('tr-TR', {
hour: '2-digit',
minute: '2-digit',
})
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<FaUsers className="text-blue-600" size={24} />
<h2 className="text-2xl font-bold text-gray-800">Katılım Raporu</h2>
</div>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 text-xl font-bold"
>
×
</button>
</div>
</div>
<div className="p-6 overflow-y-auto max-h-96">
{attendanceRecords.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<FaClock size={48} className="mx-auto mb-4" />
<p>Henüz katılım kaydı bulunmamaktadır.</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse border border-gray-300">
<thead>
<tr className="bg-gray-50">
<th className="border border-gray-300 px-4 py-3 text-left font-semibold text-gray-700">
Öğrenci Adı
</th>
<th className="border border-gray-300 px-4 py-3 text-left font-semibold text-gray-700">
Giriş Saati
</th>
<th className="border border-gray-300 px-4 py-3 text-left font-semibold text-gray-700">
Çıkış Saati
</th>
<th className="border border-gray-300 px-4 py-3 text-left font-semibold text-gray-700">
Toplam Süre
</th>
</tr>
</thead>
<tbody>
{attendanceRecords.map((record) => (
<tr key={record.id} className="hover:bg-gray-50">
<td className="border border-gray-300 px-4 py-3 text-gray-800">
{record.studentName}
</td>
<td className="border border-gray-300 px-4 py-3 text-gray-600">
{formatTime(record.joinTime)}
</td>
<td className="border border-gray-300 px-4 py-3 text-gray-600">
{record.leaveTime ? formatTime(record.leaveTime) : 'Devam ediyor'}
</td>
<td className="border border-gray-300 px-4 py-3 text-gray-800 font-semibold">
{(() => {
// Her zaman canlı süreyi hesapla, çıkış varsa oraya kadar, yoksa şimdiye kadar
const endTime = record.leaveTime
? new Date(record.leaveTime).getTime()
: now
const join = new Date(record.joinTime).getTime()
const mins = Math.floor((endTime - join) / 60000)
return formatDuration(mins)
})()}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,249 @@
import { ClassChatDto } 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: ClassChatDto[]
isTeacher: boolean
isOpen: boolean
onClose: () => void
onSendMessage: (message: string) => void
participants: Array<{ id: string; name: string; isTeacher: boolean }>
onSendPrivateMessage: (message: string, recipientId: string, recipientName: string) => void
onSendAnnouncement?: (message: string) => void
}
export const ChatPanel: React.FC<ChatPanelProps> = ({
messages,
isTeacher,
isOpen,
onClose,
onSendMessage,
participants,
onSendPrivateMessage,
onSendAnnouncement,
}) => {
const { user } = useStoreState((state) => state.auth)
const [newMessage, setNewMessage] = useState('')
const [messageMode, setMessageMode] = useState<'public' | 'private' | 'announcement'>('public')
const [selectedRecipient, setSelectedRecipient] = useState<{ id: string; name: string } | null>(
null,
)
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
useEffect(() => {
scrollToBottom()
}, [messages])
const handleSendMessage = (e: React.FormEvent) => {
e.preventDefault()
if (newMessage.trim()) {
if (messageMode === 'private' && selectedRecipient) {
onSendPrivateMessage(newMessage.trim(), selectedRecipient.id, selectedRecipient.name)
} else if (messageMode === 'announcement' && onSendAnnouncement) {
onSendAnnouncement(newMessage.trim())
} else {
onSendMessage(newMessage.trim())
}
setNewMessage('')
}
}
const formatTime = (timestamp: string) => {
return new Date(timestamp).toLocaleTimeString('tr-TR', {
hour: '2-digit',
minute: '2-digit',
})
}
if (!isOpen) return null
const availableRecipients = participants.filter((p) => p.id !== user.id)
return (
<div className="fixed right-4 bottom-4 w-96 h-[500px] bg-white rounded-lg shadow-xl border border-gray-200 flex flex-col z-50">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-blue-50 rounded-t-lg">
<div className="flex items-center space-x-2">
<FaComments className="text-blue-600" size={20} />
<h3 className="font-semibold text-gray-800">Sınıf Sohbeti</h3>
</div>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 transition-colors">
<FaTimes size={18} />
</button>
</div>
{/* Message Mode Selector */}
<div className="p-3 border-b border-gray-200 bg-gray-50">
<div className="flex space-x-2 mb-2">
<button
onClick={() => {
setMessageMode('public')
setSelectedRecipient(null)
}}
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
messageMode === 'public'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
<FaUsers size={12} />
<span>Herkese</span>
</button>
<button
onClick={() => setMessageMode('private')}
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
messageMode === 'private'
? 'bg-green-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
<FaUser size={12} />
<span>Özel</span>
</button>
{isTeacher && (
<button
onClick={() => {
setMessageMode('announcement')
setSelectedRecipient(null)
}}
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
messageMode === 'announcement'
? 'bg-red-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
<FaBullhorn size={12} />
<span>Duyuru</span>
</button>
)}
</div>
{messageMode === 'private' && (
<select
value={selectedRecipient?.id || ''}
onChange={(e) => {
const recipient = availableRecipients.find((p) => p.id === e.target.value)
setSelectedRecipient(recipient ? { id: recipient.id, name: recipient.name } : null)
}}
className="w-full px-2 py-1 text-xs border border-gray-300 rounded"
>
<option value="">Kişi seçin...</option>
{availableRecipients.map((participant) => (
<option key={participant.id} value={participant.id}>
{participant.name} {participant.isTeacher ? '(Öğretmen)' : ''}
</option>
))}
</select>
)}
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{messages.length === 0 ? (
<div className="text-center text-gray-500 text-sm">Henüz mesaj bulunmamaktadır.</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={`${
message.messageType === 'announcement'
? 'w-full'
: message.senderId === user.id
? 'flex justify-end'
: 'flex justify-start'
}`}
>
<div
className={`max-w-xs px-3 py-2 rounded-lg ${
message.messageType === 'announcement'
? 'bg-red-100 text-red-800 border border-red-200 w-full text-center'
: message.messageType === 'private'
? message.senderId === user.id
? 'bg-green-600 text-white'
: 'bg-green-100 text-green-800 border border-green-200'
: message.senderId === user.id
? 'bg-blue-600 text-white'
: message.isTeacher
? 'bg-yellow-100 text-yellow-800 border border-yellow-200'
: 'bg-gray-100 text-gray-800'
}`}
>
{message.senderId !== user.id && (
<div className="text-xs font-semibold mb-1">
{message.senderName}
{message.isTeacher && ' (Öğretmen)'}
{message.messageType === 'private' &&
message.recipientId === user.id &&
' (Size özel)'}
</div>
)}
{message.messageType === 'private' && message.senderId === user.id && (
<div className="text-xs mb-1 opacity-75"> {message.recipientName}</div>
)}
{message.messageType === 'announcement' && (
<div className="text-xs font-semibold mb-1">📢 DUYURU - {message.senderName}</div>
)}
<div className="text-sm">{message.message}</div>
<div
className={`text-xs mt-1 opacity-75 ${
message.messageType === 'announcement'
? 'text-red-600'
: message.senderId === user.id
? 'text-white'
: 'text-gray-500'
}`}
>
{formatTime(message.timestamp)}
</div>
</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
{/* Message Input */}
<form onSubmit={handleSendMessage} className="p-4 border-t border-gray-200">
<div className="text-xs text-gray-500 mb-2">
{messageMode === 'public' && 'Herkese mesaj gönderiyorsunuz'}
{messageMode === 'private' &&
selectedRecipient &&
`${selectedRecipient.name} kişisine özel mesaj`}
{messageMode === 'private' && !selectedRecipient && 'Önce bir kişi seçin'}
{messageMode === 'announcement' && 'Sınıfa duyuru gönderiyorsunuz'}
</div>
<div className="flex space-x-2">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder={
messageMode === 'private' && !selectedRecipient
? 'Önce kişi seçin...'
: messageMode === 'announcement'
? 'Duyuru mesajınız...'
: 'Mesajınızı yazın...'
}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
maxLength={500}
disabled={messageMode === 'private' && !selectedRecipient}
/>
<button
type="submit"
disabled={!newMessage.trim() || (messageMode === 'private' && !selectedRecipient)}
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
<FaPaperPlane size={16} />
</button>
</div>
</form>
</div>
)
}

View file

@ -0,0 +1,116 @@
import { VideoLayoutDto } from '@/proxy/classroom/models'
import React from 'react'
import { FaExpand, FaTh, FaColumns, FaDesktop, FaChalkboardTeacher } from 'react-icons/fa'
interface ClassLayoutPanelProps {
currentLayout: VideoLayoutDto
isOpen: boolean
onClose: () => void
}
const layouts: VideoLayoutDto[] = [
{
id: 'grid',
name: 'Izgara Görünümü',
type: 'grid',
description: 'Tüm katılımcılar eşit boyutta görünür',
},
{
id: 'sidebar',
name: 'Yan Panel Görünümü',
type: 'sidebar',
description: 'Ana konuşmacı büyük, diğerleri yan panelde',
},
{
id: 'teacher-focus',
name: 'Öğretmen Odaklı',
type: 'teacher-focus',
description: 'Öğretmen tam ekranda görünür, öğrenciler küçük panelde',
},
]
export const ClassLayoutPanel: React.FC<ClassLayoutPanelProps> = ({
currentLayout,
isOpen,
onClose,
}) => {
const getLayoutIcon = (type: string) => {
switch (type) {
case 'grid':
return <FaTh size={24} />
case 'speaker':
return <FaExpand size={24} />
case 'presentation':
return <FaDesktop size={24} />
case 'sidebar':
return <FaColumns size={24} />
case 'teacher-focus':
// Sade, tek kişilik bir ikon (öğretmen)
return <FaChalkboardTeacher size={26} />
default:
return <FaTh size={24} />
}
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900">Video Layout Seçin</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 text-xl font-bold">
×
</button>
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{layouts.map((layout) => (
<div key={layout.id}>
<div className="flex items-center space-x-4 mb-3">
<div
className={`p-3 rounded-full ${
currentLayout.id === layout.id
? 'bg-blue-100 text-blue-600'
: 'bg-gray-100 text-gray-600'
}`}
>
{getLayoutIcon(layout.type)}
</div>
<div>
<h3 className="font-semibold text-gray-900">{layout.name}</h3>
<p className="text-sm text-gray-600">{layout.description}</p>
</div>
</div>
<div className="bg-gray-100 rounded-lg p-4 h-24 flex items-center justify-center">
{layout.type === 'grid' && (
<div className="grid grid-cols-2 gap-1">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="w-6 h-4 bg-blue-300 rounded"></div>
))}
</div>
)}
{layout.type === 'sidebar' && (
<div className="flex items-center space-x-2">
<div className="w-12 h-8 bg-blue-500 rounded"></div>
<div className="grid grid-cols-3 gap-1">
<div className="w-1 h-1 bg-blue-300 rounded"></div>
<div className="w-1 h-1 bg-blue-300 rounded"></div>
<div className="w-1 h-1 bg-blue-300 rounded"></div>
<div className="w-1 h-1 bg-blue-300 rounded"></div>
<div className="w-1 h-1 bg-blue-300 rounded"></div>
<div className="w-1 h-1 bg-blue-300 rounded"></div>
</div>
</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,244 @@
import React, { useState, useRef } from 'react'
import { motion } from 'framer-motion'
import {
FaFile,
FaUpload,
FaDownload,
FaTrash,
FaEye,
FaTimes,
FaFilePdf,
FaFileWord,
FaFileImage,
FaFileAlt,
FaPlay,
FaStop,
} from 'react-icons/fa'
import { ClassDocumentDto } from '@/proxy/classroom/models'
interface DocumentPanelProps {
documents: ClassDocumentDto[]
isOpen: boolean
onClose: () => void
onUpload?: (file: File) => void
onDelete?: (documentId: string) => void
onView?: (document: ClassDocumentDto) => void
isTeacher: boolean
onStartPresentation?: (document: ClassDocumentDto) => void
onStopPresentation?: () => void
activePresentationId?: string
}
export const DocumentPanel: React.FC<DocumentPanelProps> = ({
documents,
isOpen,
onClose,
onUpload,
onDelete,
onView,
isTeacher,
onStartPresentation,
onStopPresentation,
activePresentationId,
}) => {
const [dragOver, setDragOver] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const getFileIcon = (type: string) => {
if (type.includes('pdf')) return <FaFilePdf className="text-red-500" />
if (
type.includes('word') ||
type.includes('doc') ||
type.includes('presentation') ||
type.includes('powerpoint')
)
return <FaFileWord className="text-blue-500" />
if (type.includes('image')) return <FaFileImage className="text-green-500" />
return <FaFileAlt className="text-gray-500" />
}
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const isPresentationFile = (type: string, name: string) => {
return (
type.includes('presentation') ||
type.includes('powerpoint') ||
name.toLowerCase().includes('.ppt') ||
name.toLowerCase().includes('.pptx') ||
type.includes('pdf')
)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(false)
if (!isTeacher || !onUpload) return
const files = Array.from(e.dataTransfer.files)
files.forEach((file) => onUpload(file))
}
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isTeacher || !onUpload) return
const files = Array.from(e.target.files || [])
files.forEach((file) => onUpload(file))
// Reset input
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-white rounded-lg max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden"
>
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<FaFile className="text-blue-600" size={24} />
<h2 className="text-2xl font-bold text-gray-800">Sınıf Dokümanları</h2>
</div>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 text-xl font-bold"
>
<FaTimes size={20} />
</button>
</div>
</div>
<div className="p-6 overflow-y-auto max-h-96">
{/* Upload Area (Teacher Only) */}
{isTeacher && (
<div
className={`border-2 border-dashed rounded-lg p-8 mb-6 text-center transition-colors ${
dragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
}`}
onDrop={handleDrop}
onDragOver={(e) => {
e.preventDefault()
setDragOver(true)
}}
onDragLeave={() => setDragOver(false)}
>
<FaUpload size={48} className="mx-auto text-gray-400 mb-4" />
<p className="text-lg font-medium text-gray-700 mb-2">Doküman Yükle</p>
<p className="text-gray-500 mb-4">Dosyaları buraya sürükleyin veya seçin</p>
<button
onClick={() => fileInputRef.current?.click()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Dosya Seç
</button>
<input
ref={fileInputRef}
type="file"
multiple
onChange={handleFileSelect}
className="hidden"
accept=".pdf,.doc,.docx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.odp"
/>
</div>
)}
{/* Documents List */}
{documents.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<FaFile size={48} className="mx-auto mb-4 text-gray-300" />
<p>Henüz doküman yüklenmemiş.</p>
</div>
) : (
<div className="grid gap-4">
{documents.map((doc) => (
<div
key={doc.id}
className="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow"
>
<div className="flex items-center space-x-4">
<div className="text-2xl">{getFileIcon(doc.type)}</div>
<div>
<h3 className="font-semibold text-gray-800">{doc.name}</h3>
<p className="text-sm text-gray-600">
{formatFileSize(doc.size)} {' '}
{new Date(doc.uploadedAt).toLocaleDateString('tr-TR')}
</p>
<p className="text-xs text-gray-500">Yükleyen: {doc.uploadedBy}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => onView?.(doc)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Görüntüle"
>
<FaEye size={16} />
</button>
{/* Sunum Başlat/Durdur Butonu */}
{isTeacher && isPresentationFile(doc.type, doc.name) && (
<button
onClick={() => {
if (activePresentationId === doc.id) {
onStopPresentation?.()
} else {
onStartPresentation?.(doc)
}
}}
className={`p-2 rounded-lg transition-colors ${
activePresentationId === doc.id
? 'text-red-600 hover:bg-red-50'
: 'text-green-600 hover:bg-green-50'
}`}
title={activePresentationId === doc.id ? 'Sunumu Durdur' : 'Sunum Başlat'}
>
{activePresentationId === doc.id ? (
<FaStop size={16} />
) : (
<FaPlay size={16} />
)}
</button>
)}
<a
href={doc.url}
download={doc.name}
className="p-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
title="İndir"
>
<FaDownload size={16} />
</a>
{isTeacher && (
<button
onClick={() => onDelete?.(doc.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Sil"
>
<FaTrash size={16} />
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
</motion.div>
</div>
)
}

View file

@ -0,0 +1,113 @@
import { HandRaiseDto } from '@/proxy/classroom/models'
import React from 'react'
import { FaHandPaper, FaTimes, FaCheck } from 'react-icons/fa'
interface HandRaisePanelProps {
handRaises: HandRaiseDto[]
isOpen: boolean
onClose: () => void
onApprove?: (handRaiseId: string) => void
onDismiss?: (handRaiseId: string) => void
isTeacher: boolean
}
export const HandRaisePanel: React.FC<HandRaisePanelProps> = ({
handRaises,
isOpen,
onClose,
onApprove,
onDismiss,
isTeacher,
}) => {
const formatTime = (timestamp: string) => {
return new Date(timestamp).toLocaleTimeString('tr-TR', {
hour: '2-digit',
minute: '2-digit',
})
}
const getTimeSince = (timestamp: string) => {
const now = new Date()
const time = new Date(timestamp)
const diffMinutes = Math.floor((now.getTime() - time.getTime()) / 60000)
if (diffMinutes < 1) return 'Az önce'
if (diffMinutes < 60) return `${diffMinutes} dakika önce`
const hours = Math.floor(diffMinutes / 60)
return `${hours} saat önce`
}
if (!isOpen) return null
const activeHandRaises = handRaises.filter((hr) => hr.isActive)
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg max-w-2xl w-full mx-4 max-h-[80vh] overflow-hidden">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<FaHandPaper className="text-yellow-600" size={24} />
<h2 className="text-2xl font-bold text-gray-800">
Parmak Kaldıranlar ({activeHandRaises.length})
</h2>
</div>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 text-xl font-bold"
>
×
</button>
</div>
</div>
<div className="p-6 overflow-y-auto max-h-96">
{activeHandRaises.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<FaHandPaper size={48} className="mx-auto mb-4 text-gray-300" />
<p>Şu anda parmak kaldıran öğrenci bulunmamaktadır.</p>
</div>
) : (
<div className="space-y-3">
{activeHandRaises.map((handRaise) => (
<div
key={handRaise.id}
className="flex items-center justify-between p-4 bg-yellow-50 border border-yellow-200 rounded-lg"
>
<div className="flex items-center space-x-3">
<FaHandPaper className="text-yellow-600" size={20} />
<div>
<h3 className="font-semibold text-gray-800">{handRaise.studentName}</h3>
<p className="text-sm text-gray-600">
{formatTime(handRaise.timestamp)} {getTimeSince(handRaise.timestamp)}
</p>
</div>
</div>
{isTeacher && (
<div className="flex space-x-2">
<button
onClick={() => onApprove?.(handRaise.id)}
className="flex items-center space-x-1 px-3 py-1 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm"
>
<FaCheck size={14} />
<span>Onayla</span>
</button>
<button
onClick={() => onDismiss?.(handRaise.id)}
className="flex items-center space-x-1 px-3 py-1 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm"
>
<FaTimes size={14} />
<span>Reddet</span>
</button>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,76 @@
import React from 'react';
import { FaDesktop, FaStop, FaPlay } from 'react-icons/fa';
interface ScreenSharePanelProps {
isSharing: boolean;
onStartShare: () => void;
onStopShare: () => void;
sharedScreen?: MediaStream;
sharerName?: string;
}
export const ScreenSharePanel: React.FC<ScreenSharePanelProps> = ({
isSharing,
onStartShare,
onStopShare,
sharedScreen,
sharerName,
}) => {
return (
<div className="bg-white rounded-lg shadow-md p-4 mb-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<FaDesktop className="text-blue-600" size={20} />
<h3 className="font-semibold text-gray-800">Ekran Paylaşımı</h3>
</div>
{isSharing ? (
<button
onClick={onStopShare}
className="flex items-center space-x-2 px-3 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
>
<FaStop size={16} />
<span>Paylaşımı Durdur</span>
</button>
) : (
<button
onClick={onStartShare}
className="flex items-center space-x-2 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlay size={16} />
<span>Ekranı Paylaş</span>
</button>
)}
</div>
{sharedScreen && (
<div className="relative bg-gray-900 rounded-lg overflow-hidden aspect-video">
<video
autoPlay
playsInline
muted
className="w-full h-full object-contain"
ref={(video) => {
if (video && sharedScreen) {
video.srcObject = sharedScreen;
}
}}
/>
{sharerName && (
<div className="absolute bottom-2 left-2 bg-black bg-opacity-50 text-white px-2 py-1 rounded text-sm">
{sharerName} ekranını paylaşıyor
</div>
)}
</div>
)}
{isSharing && !sharedScreen && (
<div className="bg-gray-100 rounded-lg p-8 text-center">
<FaDesktop size={48} className="mx-auto text-gray-400 mb-4" />
<p className="text-gray-600">Ekran paylaşımı başlatılıyor...</p>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,297 @@
import React from 'react'
import { FaMicrophoneSlash, FaExpand, FaUserTimes } from 'react-icons/fa'
import { VideoPlayer } from './VideoPlayer'
import { ClassParticipantDto, VideoLayoutDto } from '@/proxy/classroom/models'
interface ParticipantGridProps {
participants: ClassParticipantDto[]
localStream?: MediaStream
currentUserId: string
currentUserName: string
isTeacher: boolean
isAudioEnabled: boolean
isVideoEnabled: boolean
onToggleAudio: () => void
onToggleVideo: () => void
onLeaveCall: () => void
onMuteParticipant?: (participantId: string, isMuted: boolean) => void
layout: VideoLayoutDto
focusedParticipant?: string
onParticipantFocus?: (participantId: string | undefined) => void
onKickParticipant?: (participantId: string) => void
hasSidePanel?: boolean
}
export const ParticipantGrid: React.FC<ParticipantGridProps> = ({
participants,
localStream,
currentUserId,
currentUserName,
isTeacher,
isAudioEnabled,
isVideoEnabled,
onToggleAudio,
onToggleVideo,
onLeaveCall,
onMuteParticipant,
layout,
focusedParticipant,
onParticipantFocus,
onKickParticipant,
hasSidePanel = false,
}) => {
// Only show current user's video once
const currentUserParticipant = {
id: currentUserId,
name: currentUserName,
isTeacher,
stream: localStream,
}
// Eğer hiç katılımcı yoksa ve localStream de yoksa hiçbir şey render etme
if (!localStream && (!participants || participants.length === 0)) {
return null
}
const allParticipants = [currentUserParticipant, ...participants]
// Ortak ana video kutusu container class'ı
const mainVideoContainerClass = 'w-full h-full flex flex-col justify-center'
const renderGridLayout = () => {
const getGridClass = (participantCount: number) => {
if (participantCount === 1) return 'grid-cols-1'
if (participantCount <= 2) return 'grid-cols-1 sm:grid-cols-2'
if (participantCount <= 4) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-2'
if (participantCount <= 6) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
if (participantCount <= 9) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
return 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
}
const getGridRows = (participantCount: number) => {
if (participantCount === 1) return 'grid-rows-1'
if (participantCount <= 2) return 'grid-rows-1 sm:grid-rows-1'
if (participantCount <= 4) return 'grid-rows-2 lg:grid-rows-2'
if (participantCount <= 6) return 'grid-rows-3 sm:grid-rows-2'
if (participantCount <= 9) return 'grid-rows-3'
return 'grid-rows-4 sm:grid-rows-3'
}
const getPadding = (participantCount: number) => {
if (participantCount === 1) return ''
if (participantCount <= 4) return 'p-2 sm:p-4'
return 'p-1 sm:p-2'
}
const getGap = (participantCount: number) => {
if (participantCount === 1) return 'gap-0'
if (participantCount <= 4) return 'gap-2 sm:gap-3'
return 'gap-1 sm:gap-2'
}
// Mobilde: En üstte öğretmen, altında katılımcılar 2'li grid ve dikey scroll
const mainParticipant = allParticipants[0]
const otherParticipants = allParticipants.slice(1)
return (
<>
{/* Mobil özel layout */}
<div className="sm:hidden w-full h-full flex flex-col items-center overflow-hidden p-2">
{/* Ana katılımcı */}
<div className="w-full max-w-md mx-auto flex-none flex items-center justify-center mb-2">
<div className="w-full aspect-video max-h-[40vh] rounded-xl overflow-hidden flex bg-white/10 shadow-md border border-white/10">
{renderParticipant(mainParticipant, true)}
</div>
</div>
{/* Diğer katılımcılar 2'li grid ve dikey scroll */}
{otherParticipants.length > 0 && (
<div
className="w-full max-w-md mx-auto flex-1 overflow-y-auto grid grid-cols-1 gap-2 pb-2 min-h-0"
style={{ maxHeight: '55vh' }}
>
{Array.from({ length: Math.ceil(otherParticipants.length / 2) }).map((_, rowIdx) => (
<div key={rowIdx} className="flex gap-2">
{otherParticipants.slice(rowIdx * 2, rowIdx * 2 + 2).map((participant) => (
<div
key={participant.id}
className="flex-1 aspect-video rounded-lg overflow-hidden flex bg-white/10 shadow border border-white/10"
>
{renderParticipant(participant, false, true)}
</div>
))}
{otherParticipants.length % 2 === 1 &&
rowIdx === Math.floor(otherParticipants.length / 2) ? (
<div className="flex-1" />
) : null}
</div>
))}
</div>
)}
</div>
{/* Masaüstü ve tablet için eski grid layout */}
<div className="hidden sm:flex h-full items-center justify-center overflow-hidden">
<div
className={`w-full h-full flex flex-col justify-center ${getPadding(allParticipants.length)}`}
>
<div
className={`h-full grid ${getGridClass(allParticipants.length)} ${getGridRows(allParticipants.length)} ${getGap(allParticipants.length)} place-items-stretch`}
>
{allParticipants.map((participant) => (
<div
key={participant.id}
className="w-full h-full max-h-full flex items-stretch justify-stretch min-h-0"
>
<div className="w-full h-full rounded-lg sm:rounded-xl overflow-hidden flex">
{renderParticipant(participant, false)}
</div>
</div>
))}
</div>
</div>
</div>
</>
)
}
const renderSidebarLayout = () => {
const mainParticipant = focusedParticipant
? allParticipants.find((p) => p.id === focusedParticipant) || allParticipants[0]
: allParticipants[0]
const otherParticipants = allParticipants.filter((p) => p.id !== mainParticipant.id)
const sidebarWidth = hasSidePanel
? 'w-20 sm:w-24 md:w-32 lg:w-40'
: 'w-24 sm:w-32 md:w-40 lg:w-48'
// Eğer hiç katılımcı yoksa, video player öğretmen odaklı gibi ortalanır ve geniş olur
return (
<div className="h-screen flex items-center justify-center p-0">
<div className={mainVideoContainerClass + ' h-full'}>
<div className="flex h-full">
<div className={`flex-1 min-w-0 flex items-center justify-center`}>
<div className="w-full h-full flex items-center justify-center">
<div className="w-full h-full rounded-xl overflow-hidden transition-all duration-200">
{renderParticipant(mainParticipant, true)}
</div>
</div>
</div>
{otherParticipants.length > 0 && (
<div className={`${sidebarWidth} p-2 overflow-y-auto rounded-l-lg h-full`}>
<div className="flex flex-col gap-2 h-full min-w-0">
{otherParticipants.map((participant) => (
<div
key={participant.id}
className="rounded-lg border border-blue-300/40 shadow shadow-blue-200/20 backdrop-blur-sm transition-all duration-200"
>
{renderParticipant(participant, false, true)}
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
}
const renderTeacherFocusLayout = () => {
// Sadece öğretmen gösterilecek, katılımcılar asla gösterilmeyecek
const teacher = allParticipants.find((p) => p.isTeacher) || allParticipants[0]
return (
<div className="h-full flex items-center justify-center overflow-hidden">
<div className="w-full h-full flex flex-col justify-center ">
<div className="h-full w-full max-h-full flex items-center justify-center">
<div className="w-full h-full rounded-lg sm:rounded-xl overflow-hidden">
{renderParticipant(teacher, true)}
</div>
</div>
</div>
</div>
)
}
const renderParticipant = (
participant: ClassParticipantDto,
isMain: boolean = false,
isSmall: boolean = false,
) => (
<div
key={participant.id}
className={`relative w-full h-full ${isMain ? '' : isSmall ? 'aspect-video' : ''} ${!isMain && onParticipantFocus ? 'cursor-pointer' : ''}`}
onClick={() => !isMain && onParticipantFocus?.(participant.id)}
style={{ minHeight: 0, minWidth: 0 }}
>
<div className="absolute inset-0 w-full h-full">
<VideoPlayer
stream={participant.stream}
isLocal={participant.id === currentUserId}
userName={participant.name}
isAudioEnabled={
participant.id === currentUserId ? isAudioEnabled : !participant.isAudioMuted
}
isVideoEnabled={
participant.id === currentUserId ? isVideoEnabled : !participant.isVideoMuted
}
onToggleAudio={participant.id === currentUserId ? onToggleAudio : undefined}
onToggleVideo={participant.id === currentUserId ? onToggleVideo : undefined}
onLeaveCall={participant.id === currentUserId ? onLeaveCall : undefined}
/>
</div>
{/* Teacher controls for students */}
{isTeacher && participant.id !== currentUserId && (
<div className="absolute top-2 left-2 flex space-x-1 z-10">
<button
onClick={(e) => {
e.stopPropagation()
onMuteParticipant?.(participant.id, !participant.isAudioMuted)
}}
className={`p-1 rounded-full text-white text-xs ${
participant.isAudioMuted ? 'bg-red-600' : 'bg-gray-600 hover:bg-gray-700'
} transition-colors`}
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
>
<FaMicrophoneSlash size={12} />
</button>
<button
onClick={(e) => {
e.stopPropagation()
onKickParticipant?.(participant.id)
}}
className="p-1 rounded-full bg-red-600 hover:bg-red-700 text-white text-xs transition-colors"
title="Sınıftan Çıkar"
>
<FaUserTimes size={12} />
</button>
</div>
)}
{/* Expand button for non-main participants */}
{!isMain && onParticipantFocus && (
<div className="absolute top-2 right-2 z-10">
<button
onClick={(e) => {
e.stopPropagation()
onParticipantFocus(participant.id)
}}
className="p-1 bg-black bg-opacity-50 text-white rounded-full hover:bg-opacity-70 transition-all"
title="Büyüt"
>
<FaExpand size={12} />
</button>
</div>
)}
</div>
)
const renderLayout = () => {
switch (layout.type) {
case 'sidebar':
return renderSidebarLayout()
case 'teacher-focus':
return renderTeacherFocusLayout()
default:
return renderGridLayout()
}
}
return <div className="h-full min-h-0 flex flex-col">{renderLayout()}</div>
}

View file

@ -0,0 +1,65 @@
import React from 'react'
import { motion } from 'framer-motion'
import { FaGraduationCap, FaUserCheck, FaEye } from 'react-icons/fa'
interface RoleSelectorProps {
onRoleSelect: (role: 'teacher' | 'student' | 'observer') => void
}
export const RoleSelector: React.FC<RoleSelectorProps> = ({ onRoleSelect }) => {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 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"
>
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-800 mb-4">
Sanal Sınıf Sistemine Hoş Geldiniz
</h1>
<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>
)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,72 @@
import React, { useRef, useEffect } from 'react'
import { FaMicrophoneSlash, FaVideoSlash } from 'react-icons/fa'
// VideoOff component replacement
const VideoOff: React.FC<{ size?: number; className?: string }> = ({
size = 24,
className = '',
}) => <FaVideoSlash size={size} className={className} />
interface VideoPlayerProps {
stream?: MediaStream
isLocal?: boolean
userName: string
isAudioEnabled?: boolean
isVideoEnabled?: boolean
onToggleAudio?: () => void
onToggleVideo?: () => void
onLeaveCall?: () => void
}
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
stream,
isLocal = false,
userName,
isAudioEnabled = true,
isVideoEnabled = true,
onToggleAudio,
onToggleVideo,
onLeaveCall,
}) => {
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
if (videoRef.current && stream) {
videoRef.current.srcObject = stream
}
}, [stream])
return (
<div className="relative bg-gray-900 rounded-md sm:rounded-lg overflow-hidden p-1 sm:p-2 h-full">
<video
ref={videoRef}
autoPlay
playsInline
muted={isLocal}
className="w-full h-full object-cover"
/>
{/* User name overlay */}
<div className="absolute bottom-1 sm:bottom-2 left-1 sm:left-2 bg-black bg-opacity-50 text-white px-1 sm:px-2 py-0.5 sm:py-1 rounded text-xs sm:text-sm">
{userName} {isLocal && '(You)'}
</div>
{/* Video disabled overlay */}
{!isVideoEnabled && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-800">
<div className="text-center text-white">
<VideoOff size={24} className="mx-auto mb-1 sm:mb-2 text-white sm:size-8" />
<p className="text-xs sm:text-sm">{userName}</p>
</div>
</div>
)}
{/* Audio indicator */}
{!isAudioEnabled && (
<div className="absolute top-1 sm:top-2 right-1 sm:right-2 bg-red-500 rounded-full p-0.5 sm:p-1">
<FaMicrophoneSlash size={12} className="text-white sm:size-4" />
</div>
)}
</div>
)
}

View file

@ -0,0 +1,47 @@
export const initialScheduledClasses = [
{
id: '1',
name: 'Matematik 101 - Diferansiyel Denklemler',
description: 'İleri matematik konuları ve uygulamaları',
teacherId: 'teacher1',
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

@ -0,0 +1,135 @@
export type Role = 'teacher' | 'student' | 'observer'
export interface User {
id: string
name: string
email: string
role: Role
}
export interface ClassroomDto {
id: string
name: string
description?: string
teacherId: string
teacherName: string
startTime: string
scheduledStartTime: string
endTime?: string
isActive: boolean
isScheduled: boolean
participantCount: number
maxParticipants?: number
subject?: string
duration?: number // in minutes
settings?: ClassroomSettingsDto
}
export interface ClassroomSettingsDto {
allowHandRaise: boolean
defaultMicrophoneState: 'muted' | 'unmuted'
defaultCameraState: 'on' | 'off'
defaultLayout: string
allowStudentScreenShare: boolean
allowStudentChat: boolean
allowPrivateMessages: boolean
autoMuteNewParticipants: boolean
recordSession: boolean
waitingRoomEnabled: boolean
}
export interface ClassAttendanceDto {
id: string
sessionId: string
studentId: string
studentName: string
joinTime: string
leaveTime?: string
totalDurationMinutes: number
}
export type MediaType = 'audio' | 'video' | 'screen'
export interface SignalingMessageDto {
type: MediaType
fromUserId: string
toUserId: string
data: any
}
export interface ClassParticipantDto {
id: string
name: string
isTeacher: boolean
isObserver?: boolean
isAudioMuted?: boolean
isVideoMuted?: boolean
stream?: MediaStream
screenStream?: MediaStream
isScreenSharing?: boolean
peerConnection?: RTCPeerConnection
}
export type messageType = 'public' | 'private' | 'announcement'
export interface ClassChatDto {
id: string
senderId: string
senderName: string
message: string
timestamp: string
isTeacher: boolean
recipientId?: string // Özel mesaj için
recipientName?: string
messageType: messageType
}
export type VideoLayoutType = 'grid' | 'sidebar' | 'teacher-focus'
export interface VideoLayoutDto {
id: string
name: string
type: VideoLayoutType
description: string
}
export interface TeacherLayoutDto extends VideoLayoutDto {
id: 'teacher-focus'
name: 'Öğretmen Odaklı'
type: 'teacher-focus'
description: 'Öğretmen tam ekranda, öğrenciler küçük panelde'
}
export interface ScheduledClassDto {
id: string
name: string
scheduledTime: string
duration: number
canJoin: boolean
}
export interface HandRaiseDto {
id: string
studentId: string
studentName: string
timestamp: string
isActive: boolean
}
export interface ClassDocumentDto {
id: string
name: string
url: string
type: string
size: number
uploadedAt: string
uploadedBy: string
isPresentation?: boolean
totalPages?: number
}
export interface ScreenShareRequestDto {
userId: string
userName: string
isActive: boolean
}

View file

@ -76,6 +76,11 @@ export const ROUTES_ENUM = {
formEdit: '/admin/form/:listFormCode/:id/edit',
chart: '/admin/chart/:chartCode',
pivot: '/admin/pivot/:listFormCode',
classroom: {
dashboard: '/admin/classroom/dashboard',
classes: '/admin/classroom/classes',
classroom: '/admin/classroom/room/:id',
},
},
accessDenied: '/admin/access-denied',
},

View file

@ -0,0 +1,394 @@
import {
ClassAttendanceDto,
ClassChatDto,
HandRaiseDto,
SignalingMessageDto,
} from '@/proxy/classroom/models'
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 onSignalingMessage?: (message: SignalingMessageDto) => void
private onAttendanceUpdate?: (record: ClassAttendanceDto) => void
private onParticipantJoined?: (userId: string, name: string) => void
private onParticipantLeft?: (userId: string) => void
private onChatMessage?: (message: ClassChatDto) => void
private onParticipantMuted?: (userId: string, isMuted: boolean) => void
private onHandRaiseReceived?: (handRaise: HandRaiseDto) => void
private onHandRaiseDismissed?: (handRaiseId: string) => void
constructor() {
// Only initialize connection if not in demo mode
if (!this.demoMode) {
// In production, replace with your actual SignalR hub URL
this.connection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:5001/classroomhub', {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.withAutomaticReconnect()
.build()
this.setupEventHandlers()
}
}
private setupEventHandlers() {
if (this.demoMode || !this.connection) return
this.connection.on('ReceiveSignalingMessage', (message: SignalingMessageDto) => {
this.onSignalingMessage?.(message)
})
this.connection.on('AttendanceUpdated', (record: ClassAttendanceDto) => {
this.onAttendanceUpdate?.(record)
})
this.connection.on('ParticipantJoined', (userId: string, name: string) => {
this.onParticipantJoined?.(userId, name)
})
this.connection.on('ParticipantLeft', (userId: string) => {
this.onParticipantLeft?.(userId)
})
this.connection.on('ChatMessage', (message: any) => {
this.onChatMessage?.(message)
})
this.connection.on('ParticipantMuted', (userId: string, isMuted: boolean) => {
this.onParticipantMuted?.(userId, isMuted)
})
this.connection.on('HandRaiseReceived', (handRaise: HandRaiseDto) => {
this.onHandRaiseReceived?.(handRaise)
})
this.connection.on('HandRaiseDismissed', (handRaiseId: string) => {
this.onHandRaiseDismissed?.(handRaiseId)
})
this.connection.onreconnected(() => {
console.log('SignalR reconnected')
})
this.connection.onclose(() => {
console.log('SignalR connection closed')
})
}
async start(): Promise<void> {
if (this.demoMode) {
console.log('SignalR running in demo mode - no backend connection required')
return
}
try {
await this.connection.start()
this.isConnected = true
console.log('SignalR connection started')
} catch (error) {
console.error('Error starting SignalR connection:', error)
// Switch to demo mode if connection fails
this.demoMode = true
this.isConnected = false
console.log('Switched to demo mode - SignalR simulation active')
}
}
async joinClass(sessionId: string, userId: string, userName: string): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating join class for', userName)
// Simulate successful join in demo mode
// Don't auto-add participants in demo mode - let manual simulation handle this
return
}
try {
await this.connection.invoke('JoinClass', sessionId, userId, userName)
} catch (error) {
console.error('Error joining class:', error)
}
}
async leaveClass(sessionId: string, userId: string): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating leave class for user', userId)
// Simulate successful leave in demo mode
setTimeout(() => {
this.onParticipantLeft?.(userId)
}, 100)
return
}
try {
await this.connection.invoke('LeaveClass', sessionId, userId)
} catch (error) {
console.error('Error leaving class:', error)
}
}
async sendSignalingMessage(message: SignalingMessageDto): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating signaling message', message.type)
// In demo mode, we can't send real signaling messages
// WebRTC will need to work in local-only mode
return
}
try {
await this.connection.invoke('SendSignalingMessage', message)
} catch (error) {
console.error('Error sending signaling message:', error)
}
}
async sendChatMessage(
sessionId: string,
senderId: string,
senderName: string,
message: string,
isTeacher: boolean,
): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating chat message from', senderName)
const chatMessage: ClassChatDto = {
id: `msg-${Date.now()}`,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
messageType: 'public',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendChatMessage',
sessionId,
senderId,
senderName,
message,
isTeacher,
'public',
)
} catch (error) {
console.error('Error sending chat message:', error)
}
}
async sendPrivateMessage(
sessionId: string,
senderId: string,
senderName: string,
message: string,
recipientId: string,
recipientName: string,
isTeacher: boolean,
): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating private message from', senderName, 'to', recipientName)
const chatMessage: ClassChatDto = {
id: `msg-${Date.now()}`,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
recipientId,
recipientName,
messageType: 'private',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendPrivateMessage',
sessionId,
senderId,
senderName,
message,
recipientId,
recipientName,
isTeacher,
)
} catch (error) {
console.error('Error sending private message:', error)
}
}
async sendAnnouncement(
sessionId: string,
senderId: string,
senderName: string,
message: string,
): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating announcement from', senderName)
const chatMessage: ClassChatDto = {
id: `msg-${Date.now()}`,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher: true,
messageType: 'announcement',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke('SendAnnouncement', sessionId, senderId, senderName, message)
} catch (error) {
console.error('Error sending chat message:', error)
}
}
async muteParticipant(sessionId: string, userId: string, isMuted: boolean): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating mute participant', userId, isMuted)
setTimeout(() => {
this.onParticipantMuted?.(userId, isMuted)
}, 100)
return
}
try {
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted)
} catch (error) {
console.error('Error muting participant:', error)
}
}
async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating hand raise from', studentName)
const handRaise: HandRaiseDto = {
id: `hand-${Date.now()}`,
studentId,
studentName,
timestamp: new Date().toISOString(),
isActive: true,
}
setTimeout(() => {
this.onHandRaiseReceived?.(handRaise)
}, 100)
return
}
try {
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
} catch (error) {
console.error('Error raising hand:', error)
}
}
async kickParticipant(sessionId: string, participantId: string): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating kick participant', participantId)
setTimeout(() => {
this.onParticipantLeft?.(participantId)
}, 100)
return
}
try {
await this.connection.invoke('KickParticipant', sessionId, participantId)
} catch (error) {
console.error('Error kicking participant:', error)
}
}
async approveHandRaise(sessionId: string, handRaiseId: string): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating hand raise approval')
setTimeout(() => {
this.onHandRaiseDismissed?.(handRaiseId)
}, 100)
return
}
try {
await this.connection.invoke('ApproveHandRaise', sessionId, handRaiseId)
} catch (error) {
console.error('Error approving hand raise:', error)
}
}
async dismissHandRaise(sessionId: string, handRaiseId: string): Promise<void> {
if (this.demoMode || !this.isConnected) {
console.log('Demo mode: Simulating hand raise dismissal')
setTimeout(() => {
this.onHandRaiseDismissed?.(handRaiseId)
}, 100)
return
}
try {
await this.connection.invoke('DismissHandRaise', sessionId, handRaiseId)
} catch (error) {
console.error('Error dismissing hand raise:', error)
}
}
setSignalingHandler(callback: (message: SignalingMessageDto) => void) {
this.onSignalingMessage = callback
}
setAttendanceUpdatedHandler(callback: (record: ClassAttendanceDto) => void) {
this.onAttendanceUpdate = callback
}
setParticipantJoinHandler(callback: (userId: string, name: string) => void) {
this.onParticipantJoined = callback
}
setParticipantLeaveHandler(callback: (userId: string) => void) {
this.onParticipantLeft = callback
}
setChatMessageReceivedHandler(callback: (message: ClassChatDto) => void) {
this.onChatMessage = callback
}
setParticipantMutedHandler(callback: (userId: string, isMuted: boolean) => void) {
this.onParticipantMuted = callback
}
setHandRaiseReceivedHandler(callback: (handRaise: HandRaiseDto) => void) {
this.onHandRaiseReceived = callback
}
setHandRaiseDismissedHandler(callback: (handRaiseId: string) => void) {
this.onHandRaiseDismissed = callback
}
async disconnect(): Promise<void> {
if (this.isConnected && this.connection) {
await this.connection.stop()
this.isConnected = false
}
}
isInDemoMode(): boolean {
return this.demoMode
}
getConnectionState(): boolean {
return this.isConnected
}
}

View file

@ -0,0 +1,150 @@
export class WebRTCService {
private peerConnections: Map<string, RTCPeerConnection> = new Map()
private localStream: MediaStream | null = null
private onRemoteStream?: (userId: string, stream: MediaStream) => void
// STUN servers for NAT traversal
private rtcConfiguration: RTCConfiguration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
],
}
async initializeLocalStream(): Promise<MediaStream> {
try {
this.localStream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 },
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
})
return this.localStream
} catch (error) {
console.error('Error accessing media devices:', error)
throw error
}
}
async createPeerConnection(userId: string): Promise<RTCPeerConnection> {
const peerConnection = new RTCPeerConnection(this.rtcConfiguration)
this.peerConnections.set(userId, peerConnection)
// Add local stream tracks to peer connection
if (this.localStream) {
this.localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, this.localStream!)
})
}
// Handle remote stream
peerConnection.ontrack = (event) => {
const [remoteStream] = event.streams
console.log('Remote stream received from user:', userId)
this.onRemoteStream?.(userId, remoteStream)
}
// Handle ICE candidates
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
console.log('ICE candidate generated for user:', userId, event.candidate)
// In a real implementation, this would be sent via SignalR
}
}
peerConnection.onconnectionstatechange = () => {
console.log(`Connection state for ${userId}:`, peerConnection.connectionState)
if (peerConnection.connectionState === 'connected') {
console.log(`Successfully connected to ${userId}`)
}
}
return peerConnection
}
async createOffer(userId: string): Promise<RTCSessionDescriptionInit> {
const peerConnection = this.peerConnections.get(userId)
if (!peerConnection) throw new Error('Peer connection not found')
const offer = await peerConnection.createOffer()
await peerConnection.setLocalDescription(offer)
return offer
}
async createAnswer(
userId: string,
offer: RTCSessionDescriptionInit,
): Promise<RTCSessionDescriptionInit> {
const peerConnection = this.peerConnections.get(userId)
if (!peerConnection) throw new Error('Peer connection not found')
await peerConnection.setRemoteDescription(offer)
const answer = await peerConnection.createAnswer()
await peerConnection.setLocalDescription(answer)
return answer
}
async handleAnswer(userId: string, answer: RTCSessionDescriptionInit): Promise<void> {
const peerConnection = this.peerConnections.get(userId)
if (!peerConnection) throw new Error('Peer connection not found')
await peerConnection.setRemoteDescription(answer)
}
async addIceCandidate(userId: string, candidate: RTCIceCandidateInit): Promise<void> {
const peerConnection = this.peerConnections.get(userId)
if (!peerConnection) throw new Error('Peer connection not found')
await peerConnection.addIceCandidate(candidate)
}
onRemoteStreamReceived(callback: (userId: string, stream: MediaStream) => void) {
this.onRemoteStream = callback
}
toggleVideo(enabled: boolean): void {
if (this.localStream) {
const videoTrack = this.localStream.getVideoTracks()[0]
if (videoTrack) {
videoTrack.enabled = enabled
}
}
}
toggleAudio(enabled: boolean): void {
if (this.localStream) {
const audioTrack = this.localStream.getAudioTracks()[0]
if (audioTrack) {
audioTrack.enabled = enabled
}
}
}
getLocalStream(): MediaStream | null {
return this.localStream
}
closePeerConnection(userId: string): void {
const peerConnection = this.peerConnections.get(userId)
if (peerConnection) {
peerConnection.close()
this.peerConnections.delete(userId)
}
}
closeAllConnections(): void {
this.peerConnections.forEach((pc) => pc.close())
this.peerConnections.clear()
if (this.localStream) {
this.localStream.getTracks().forEach((track) => track.stop())
this.localStream = null
}
}
}

View file

@ -20,6 +20,7 @@ export interface AuthStoreModel {
authority: string[]
name: string
avatar?: string
role: string
}
tenant?: {
tenantId?: string
@ -56,6 +57,7 @@ export const initialState: AuthStoreModel = {
authority: [],
name: '',
avatar: '',
role: 'teacher',
},
tenant: {
tenantId: '',
@ -101,6 +103,7 @@ export const authModel: AuthModel = {
state.authority = payload.authority
state.email = payload.email
state.avatar = payload.avatar
state.role = payload.role
}),
},
tenant: {

View file

@ -0,0 +1,68 @@
import { ClassroomDto } from '@/proxy/classroom/models'
import { useStoreActions, useStoreState } from '@/store/store'
import { useState } from 'react'
export type RoleState = 'role-selection' | 'dashboard' | 'classroom'
export function useClassroomLogic() {
const { user } = useStoreState((state) => state.auth)
const { setUser } = useStoreActions((actions) => actions.auth.user)
const [roleState, setRoleState] = useState<RoleState>('role-selection')
const [currentClass, setCurrentClass] = useState<ClassroomDto | null>(null)
const [allClasses, setAllClasses] = useState<ClassroomDto[]>([])
const handleRoleSelect = (role: 'teacher' | 'student' | 'observer') => {
setUser({
...user,
role,
})
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,
id: `class-${Date.now()}`,
teacherId: '',
teacherName: '',
isActive: false,
isScheduled: true,
participantCount: 0,
} as ClassroomDto
setAllClasses((prev) => [...prev, newClass])
}
const handleEditClass = (classId: string, classData: Partial<ClassroomDto>) => {
setAllClasses((prev) => prev.map((c) => (c.id === classId ? { ...c, ...classData } : c)))
}
const handleDeleteClass = (classId: string) => {
setAllClasses((prev) => prev.filter((c) => c.id !== classId))
}
return {
appState: roleState,
setAppState: setRoleState,
currentClass,
setCurrentClass,
allClasses,
setAllClasses,
handleRoleSelect,
handleJoinClass,
handleLeaveClass,
handleCreateClass,
handleEditClass,
handleDeleteClass,
}
}

View file

@ -0,0 +1,15 @@
import { ClassList } from '@/components/classroom/ClassList'
import React from 'react'
const ClassListPage: React.FC = () => {
return (
<ClassList
onCreateClass={() => {}}
onJoinClass={() => {}}
onEditClass={() => {}}
onDeleteClass={() => {}}
/>
)
}
export default ClassListPage

View file

@ -0,0 +1,8 @@
import React from 'react'
import { Dashboard } from '@/components/classroom/Dashboard'
const DashboardPage: React.FC = () => {
return <Dashboard />
}
export default DashboardPage

View file

@ -0,0 +1,23 @@
import { Room } from '@/components/classroom/Room'
import React from 'react'
const RoomPage: React.FC = () => {
return (
<Room
classSession={{
id: '',
name: '',
teacherId: '',
teacherName: '',
startTime: '',
scheduledStartTime: '',
isActive: false,
isScheduled: false,
participantCount: 0,
}}
onLeaveClass={() => {}}
/>
)
}
export default RoomPage