Classroom : Büyük güncellemee

This commit is contained in:
Sedat ÖZTÜRK 2025-08-29 12:37:38 +03:00
parent b904b35506
commit cf3cb50e1a
25 changed files with 2156 additions and 1856 deletions

View file

@ -2,7 +2,7 @@ using System;
namespace Kurs.Platform.Classrooms;
public class ClassChatDto
public class ClassroomChatDto
{
public Guid Id { get; set; }
public Guid SessionId { get; set; }
@ -10,5 +10,8 @@ public class ClassChatDto
public string SenderName { get; set; }
public string Message { get; set; }
public DateTime Timestamp { get; set; }
public Guid? RecipientId { get; set; }
public string? RecipientName { get; set; }
public bool IsTeacher { get; set; }
public string MessageType { get; set; }
}

View file

@ -53,7 +53,7 @@ public class GetClassroomListDto : PagedAndSortedResultRequestDto
public Guid? TeacherId { get; set; }
}
public class ClassAttendanceDto : EntityDto<Guid>
public class ClassroomAttendanceDto : EntityDto<Guid>
{
public Guid SessionId { get; set; }
public Guid StudentId { get; set; }

View file

@ -2,15 +2,15 @@ using System;
namespace Kurs.Platform.Classrooms;
public class ClassParticipantDto
public class ClassroomParticipantDto
{
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 bool IsHandRaised { get; set; }
public DateTime JoinTime { get; set; }
}

View file

@ -17,5 +17,5 @@ public interface IClassroomAppService : IApplicationService
Task EndClassAsync(Guid id);
Task<ClassroomDto> JoinClassAsync(Guid id);
Task LeaveClassAsync(Guid id);
Task<List<ClassAttendanceDto>> GetAttendanceAsync(Guid sessionId);
Task<List<ClassroomAttendanceDto>> GetAttendanceAsync(Guid sessionId);
}

View file

@ -15,13 +15,13 @@ namespace Kurs.Platform.Classrooms;
public class ClassroomAppService : PlatformAppService, IClassroomAppService
{
private readonly IRepository<Classroom, Guid> _classSessionRepository;
private readonly IRepository<ClassParticipant, Guid> _participantRepository;
private readonly IRepository<ClassAttandance, Guid> _attendanceRepository;
private readonly IRepository<ClassroomParticipant, Guid> _participantRepository;
private readonly IRepository<ClassroomAttandance, Guid> _attendanceRepository;
public ClassroomAppService(
IRepository<Classroom, Guid> classSessionRepository,
IRepository<ClassParticipant, Guid> participantRepository,
IRepository<ClassAttandance, Guid> attendanceRepository)
IRepository<ClassroomParticipant, Guid> participantRepository,
IRepository<ClassroomAttandance, Guid> attendanceRepository)
{
_classSessionRepository = classSessionRepository;
_participantRepository = participantRepository;
@ -198,19 +198,18 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
if (existingParticipant == null)
{
// Add participant
var participant = new ClassParticipant(
var participant = new ClassroomParticipant(
GuidGenerator.Create(),
id,
CurrentUser.Id,
CurrentUser.Name,
CurrentUser.Email,
false // isTeacher
);
await _participantRepository.InsertAsync(participant);
// Create attendance record
var attendance = new ClassAttandance(
var attendance = new ClassroomAttandance(
GuidGenerator.Create(),
id,
CurrentUser.Id,
@ -257,7 +256,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
}
}
public async Task<List<ClassAttendanceDto>> GetAttendanceAsync(Guid sessionId)
public async Task<List<ClassroomAttendanceDto>> GetAttendanceAsync(Guid sessionId)
{
var classSession = await _classSessionRepository.GetAsync(sessionId);
@ -270,6 +269,6 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
x => x.SessionId == sessionId
);
return ObjectMapper.Map<List<ClassAttandance>, List<ClassAttendanceDto>>(attendanceRecords);
return ObjectMapper.Map<List<ClassroomAttandance>, List<ClassroomAttendanceDto>>(attendanceRecords);
}
}

View file

@ -8,8 +8,8 @@ public class ClassroomAutoMapperProfile : Profile
public ClassroomAutoMapperProfile()
{
CreateMap<Classroom, ClassroomDto>();
CreateMap<ClassAttandance, ClassAttendanceDto>();
CreateMap<ClassParticipant, ClassParticipantDto>();
CreateMap<ClassChat, ClassChatDto>();
CreateMap<ClassroomAttandance, ClassroomAttendanceDto>();
CreateMap<ClassroomParticipant, ClassroomParticipantDto>();
CreateMap<ClassroomChat, ClassroomChatDto>();
}
}

View file

@ -20,15 +20,15 @@ public class Classroom : FullAuditedEntity<Guid>
public int ParticipantCount { get; set; }
public string SettingsJson { get; set; }
public virtual ICollection<ClassParticipant> Participants { get; set; }
public virtual ICollection<ClassAttandance> AttendanceRecords { get; set; }
public virtual ICollection<ClassChat> ChatMessages { get; set; }
public virtual ICollection<ClassroomParticipant> Participants { get; set; }
public virtual ICollection<ClassroomAttandance> AttendanceRecords { get; set; }
public virtual ICollection<ClassroomChat> ChatMessages { get; set; }
protected Classroom()
{
Participants = new HashSet<ClassParticipant>();
AttendanceRecords = new HashSet<ClassAttandance>();
ChatMessages = new HashSet<ClassChat>();
Participants = new HashSet<ClassroomParticipant>();
AttendanceRecords = new HashSet<ClassroomAttandance>();
ChatMessages = new HashSet<ClassroomChat>();
}
public Classroom(
@ -56,8 +56,8 @@ public class Classroom : FullAuditedEntity<Guid>
MaxParticipants = maxParticipants;
SettingsJson = settingsJson;
Participants = new HashSet<ClassParticipant>();
AttendanceRecords = new HashSet<ClassAttandance>();
ChatMessages = new HashSet<ClassChat>();
Participants = new HashSet<ClassroomParticipant>();
AttendanceRecords = new HashSet<ClassroomAttandance>();
ChatMessages = new HashSet<ClassroomChat>();
}
}

View file

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

View file

@ -3,36 +3,45 @@ using Volo.Abp.Domain.Entities.Auditing;
namespace Kurs.Platform.Entities;
public class ClassChat : FullAuditedEntity<Guid>
public class ClassroomChat : FullAuditedEntity<Guid>
{
public Guid SessionId { get; set; }
public Guid? SenderId { get; set; }
public string SenderName { get; set; }
public string Message { get; set; }
public DateTime Timestamp { get; set; }
public Guid? RecipientId { get; set; }
public string? RecipientName { get; set; }
public bool IsTeacher { get; set; }
public string MessageType { get; set; }
// Navigation properties
public virtual Classroom Session { get; set; }
protected ClassChat()
protected ClassroomChat()
{
}
public ClassChat(
public ClassroomChat(
Guid id,
Guid sessionId,
Guid? senderId,
string senderName,
string message,
bool isTeacher
Guid? recipientId,
string recipientName,
bool isTeacher,
string messageType
) : base(id)
{
SessionId = sessionId;
SenderId = senderId;
SenderName = senderName;
Message = message;
RecipientId = recipientId;
RecipientName = recipientName;
IsTeacher = isTeacher;
Timestamp = DateTime.UtcNow;
MessageType = messageType;
}
}

View file

@ -3,41 +3,40 @@ using Volo.Abp.Domain.Entities.Auditing;
namespace Kurs.Platform.Entities;
public class ClassParticipant : FullAuditedEntity<Guid>
public class ClassroomParticipant : FullAuditedEntity<Guid>
{
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 bool IsAudioMuted { get; set; } = false;
public bool IsVideoMuted { get; set; } = false;
public bool IsHandRaised { get; set; } = false;
public DateTime JoinTime { get; set; }
public string ConnectionId { get; set; }
// Navigation properties
public virtual Classroom Session { get; set; }
protected ClassParticipant()
protected ClassroomParticipant()
{
}
public ClassParticipant(
public ClassroomParticipant(
Guid id,
Guid sessionId,
Guid? userId,
string userName,
string userEmail,
bool isTeacher
) : base(id)
{
SessionId = sessionId;
UserId = userId;
UserName = userName;
UserEmail = userEmail;
IsTeacher = isTeacher;
IsAudioMuted = false;
IsVideoMuted = false;
IsHandRaised = false;
JoinTime = DateTime.UtcNow;
}

View file

@ -98,9 +98,9 @@ public class PlatformDbContext :
public DbSet<Service> Services { 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; }
public DbSet<ClassroomParticipant> Participants { get; set; }
public DbSet<ClassroomAttandance> AttendanceRecords { get; set; }
public DbSet<ClassroomChat> ChatMessages { get; set; }
#region Entities from the modules
@ -913,13 +913,12 @@ public class PlatformDbContext :
});
// Participant
builder.Entity<ClassParticipant>(b =>
builder.Entity<ClassroomParticipant>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassParticipant), PlatformConsts.DbSchema);
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassroomParticipant), PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.UserName).IsRequired().HasMaxLength(100);
b.Property(x => x.UserEmail).HasMaxLength(200);
b.Property(x => x.ConnectionId).HasMaxLength(100);
b.HasIndex(x => x.SessionId);
@ -928,9 +927,9 @@ public class PlatformDbContext :
});
// AttendanceRecord
builder.Entity<ClassAttandance>(b =>
builder.Entity<ClassroomAttandance>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassAttandance), PlatformConsts.DbSchema);
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassroomAttandance), PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.StudentName).IsRequired().HasMaxLength(100);
@ -941,9 +940,9 @@ public class PlatformDbContext :
});
// ChatMessage
builder.Entity<ClassChat>(b =>
builder.Entity<ClassroomChat>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassChat), PlatformConsts.DbSchema);
b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassroomChat), 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("20250828112303_Initial")]
[Migration("20250829093104_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -1559,219 +1559,6 @@ namespace Kurs.Platform.Migrations
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");
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<bool>("IsTeacher")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<Guid?>("SenderId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SenderName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("SenderId");
b.HasIndex("SessionId");
b.HasIndex("Timestamp");
b.ToTable("PClassChat", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", 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("PClassParticipant", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Classroom", b =>
{
b.Property<Guid>("Id")
@ -1861,6 +1648,227 @@ namespace Kurs.Platform.Migrations
b.ToTable("PClassroom", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomAttandance", 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("PClassroomAttandance", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomChat", 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<bool>("IsTeacher")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("MessageType")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("RecipientId")
.HasColumnType("uniqueidentifier");
b.Property<string>("RecipientName")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("SenderId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SenderName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("SenderId");
b.HasIndex("SessionId");
b.HasIndex("Timestamp");
b.ToTable("PClassroomChat", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomParticipant", 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>("IsHandRaised")
.HasColumnType("bit");
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<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("PClassroomParticipant", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Contact", b =>
{
b.Property<Guid>("Id")
@ -6558,7 +6566,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Country");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b =>
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomAttandance", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("AttendanceRecords")
@ -6569,7 +6577,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b =>
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomChat", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("ChatMessages")
@ -6580,7 +6588,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b =>
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomParticipant", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("Participants")

View file

@ -1922,7 +1922,7 @@ namespace Kurs.Platform.Migrations
});
migrationBuilder.CreateTable(
name: "PClassAttandance",
name: "PClassroomAttandance",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -1942,9 +1942,9 @@ namespace Kurs.Platform.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_PClassAttandance", x => x.Id);
table.PrimaryKey("PK_PClassroomAttandance", x => x.Id);
table.ForeignKey(
name: "FK_PClassAttandance_PClassroom_SessionId",
name: "FK_PClassroomAttandance_PClassroom_SessionId",
column: x => x.SessionId,
principalTable: "PClassroom",
principalColumn: "Id",
@ -1952,7 +1952,7 @@ namespace Kurs.Platform.Migrations
});
migrationBuilder.CreateTable(
name: "PClassChat",
name: "PClassroomChat",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -1961,7 +1961,10 @@ namespace Kurs.Platform.Migrations
SenderName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Message = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false),
Timestamp = table.Column<DateTime>(type: "datetime2", nullable: false),
RecipientId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
RecipientName = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsTeacher = table.Column<bool>(type: "bit", nullable: false),
MessageType = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
@ -1972,9 +1975,9 @@ namespace Kurs.Platform.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_PClassChat", x => x.Id);
table.PrimaryKey("PK_PClassroomChat", x => x.Id);
table.ForeignKey(
name: "FK_PClassChat_PClassroom_SessionId",
name: "FK_PClassroomChat_PClassroom_SessionId",
column: x => x.SessionId,
principalTable: "PClassroom",
principalColumn: "Id",
@ -1982,17 +1985,17 @@ namespace Kurs.Platform.Migrations
});
migrationBuilder.CreateTable(
name: "PClassParticipant",
name: "PClassroomParticipant",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SessionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UserName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
UserEmail = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
IsTeacher = table.Column<bool>(type: "bit", nullable: false),
IsAudioMuted = table.Column<bool>(type: "bit", nullable: false),
IsVideoMuted = table.Column<bool>(type: "bit", nullable: false),
IsHandRaised = table.Column<bool>(type: "bit", nullable: false),
JoinTime = table.Column<DateTime>(type: "datetime2", nullable: false),
ConnectionId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
@ -2005,9 +2008,9 @@ namespace Kurs.Platform.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_PClassParticipant", x => x.Id);
table.PrimaryKey("PK_PClassroomParticipant", x => x.Id);
table.ForeignKey(
name: "FK_PClassParticipant_PClassroom_SessionId",
name: "FK_PClassroomParticipant_PClassroom_SessionId",
column: x => x.SessionId,
principalTable: "PClassroom",
principalColumn: "Id",
@ -2991,53 +2994,6 @@ namespace Kurs.Platform.Migrations
columns: new[] { "CountryCode", "Code" },
unique: true);
migrationBuilder.CreateIndex(
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_ScheduledStartTime",
table: "PClassroom",
@ -3048,6 +3004,53 @@ namespace Kurs.Platform.Migrations
table: "PClassroom",
column: "TeacherId");
migrationBuilder.CreateIndex(
name: "IX_PClassroomAttandance_JoinTime",
table: "PClassroomAttandance",
column: "JoinTime");
migrationBuilder.CreateIndex(
name: "IX_PClassroomAttandance_SessionId",
table: "PClassroomAttandance",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_PClassroomAttandance_StudentId",
table: "PClassroomAttandance",
column: "StudentId");
migrationBuilder.CreateIndex(
name: "IX_PClassroomChat_SenderId",
table: "PClassroomChat",
column: "SenderId");
migrationBuilder.CreateIndex(
name: "IX_PClassroomChat_SessionId",
table: "PClassroomChat",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_PClassroomChat_Timestamp",
table: "PClassroomChat",
column: "Timestamp");
migrationBuilder.CreateIndex(
name: "IX_PClassroomParticipant_SessionId",
table: "PClassroomParticipant",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_PClassroomParticipant_SessionId_UserId",
table: "PClassroomParticipant",
columns: new[] { "SessionId", "UserId" },
unique: true,
filter: "[UserId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_PClassroomParticipant_UserId",
table: "PClassroomParticipant",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_PCountry_Code",
table: "PCountry",
@ -3303,13 +3306,13 @@ namespace Kurs.Platform.Migrations
name: "PChart");
migrationBuilder.DropTable(
name: "PClassAttandance");
name: "PClassroomAttandance");
migrationBuilder.DropTable(
name: "PClassChat");
name: "PClassroomChat");
migrationBuilder.DropTable(
name: "PClassParticipant");
name: "PClassroomParticipant");
migrationBuilder.DropTable(
name: "PContact");

View file

@ -1556,219 +1556,6 @@ namespace Kurs.Platform.Migrations
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");
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<bool>("IsTeacher")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<Guid?>("SenderId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SenderName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("SenderId");
b.HasIndex("SessionId");
b.HasIndex("Timestamp");
b.ToTable("PClassChat", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", 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("PClassParticipant", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Classroom", b =>
{
b.Property<Guid>("Id")
@ -1858,6 +1645,227 @@ namespace Kurs.Platform.Migrations
b.ToTable("PClassroom", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomAttandance", 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("PClassroomAttandance", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomChat", 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<bool>("IsTeacher")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("MessageType")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("RecipientId")
.HasColumnType("uniqueidentifier");
b.Property<string>("RecipientName")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("SenderId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SenderName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<Guid>("SessionId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("SenderId");
b.HasIndex("SessionId");
b.HasIndex("Timestamp");
b.ToTable("PClassroomChat", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomParticipant", 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>("IsHandRaised")
.HasColumnType("bit");
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<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("PClassroomParticipant", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Contact", b =>
{
b.Property<Guid>("Id")
@ -6555,7 +6563,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Country");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b =>
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomAttandance", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("AttendanceRecords")
@ -6566,7 +6574,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b =>
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomChat", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("ChatMessages")
@ -6577,7 +6585,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Session");
});
modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b =>
modelBuilder.Entity("Kurs.Platform.Entities.ClassroomParticipant", b =>
{
b.HasOne("Kurs.Platform.Entities.Classroom", "Session")
.WithMany("Participants")

View file

@ -14,16 +14,18 @@ namespace Kurs.Platform.SignalR.Hubs;
public class ClassroomHub : Hub
{
private readonly IRepository<Classroom, Guid> _classSessionRepository;
private readonly IRepository<ClassParticipant, Guid> _participantRepository;
private readonly IRepository<ClassChat, Guid> _chatMessageRepository;
private readonly IRepository<ClassroomParticipant, Guid> _participantRepository;
private readonly IRepository<ClassroomChat, Guid> _chatMessageRepository;
private readonly IRepository<ClassroomAttandance, Guid> _attendanceRepository;
private readonly ILogger<ClassroomHub> _logger;
private readonly IGuidGenerator _guidGenerator;
private readonly ICurrentUser _currentUser;
public ClassroomHub(
IRepository<Classroom, Guid> classSessionRepository,
IRepository<ClassParticipant, Guid> participantRepository,
IRepository<ClassChat, Guid> chatMessageRepository,
IRepository<ClassroomParticipant, Guid> participantRepository,
IRepository<ClassroomChat, Guid> chatMessageRepository,
IRepository<ClassroomAttandance, Guid> attendanceRepository,
ILogger<ClassroomHub> logger,
IGuidGenerator guidGenerator,
ICurrentUser currentUser)
@ -31,91 +33,93 @@ public class ClassroomHub : Hub
_classSessionRepository = classSessionRepository;
_participantRepository = participantRepository;
_chatMessageRepository = chatMessageRepository;
_attendanceRepository = attendanceRepository;
_logger = logger;
_guidGenerator = guidGenerator;
_currentUser = currentUser;
}
[HubMethodName("JoinClass")]
public async Task JoinClassAsync(Guid sessionId, string userName)
public async Task JoinClassAsync(Guid sessionId, Guid userId, string userName, bool isTeacher)
{
var classSession = await _classSessionRepository.GetAsync(sessionId);
// Add to SignalR group
await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString());
// Update participant connection
var participant = await _participantRepository.FirstOrDefaultAsync(
x => x.SessionId == sessionId && x.UserId == _currentUser.Id
x => x.SessionId == sessionId && x.UserId == userId
);
if (participant != null)
if (participant == null)
{
participant = new ClassroomParticipant(
_guidGenerator.Create(),
sessionId,
userId,
userName,
isTeacher
);
participant.UpdateConnectionId(Context.ConnectionId);
await _participantRepository.InsertAsync(participant, autoSave: true);
// 🔑 Katılımcı sayısını güncelle
var classroom = await _classSessionRepository.GetAsync(sessionId);
var participantCount = await _participantRepository.CountAsync(x => x.SessionId == sessionId);
classroom.ParticipantCount = participantCount;
await _classSessionRepository.UpdateAsync(classroom, autoSave: true);
}
else
{
participant.UpdateConnectionId(Context.ConnectionId);
await _participantRepository.UpdateAsync(participant);
await _participantRepository.UpdateAsync(participant, autoSave: true);
}
// Notify others
// 🔑 Attendance kaydı
var attendance = new ClassroomAttandance(
_guidGenerator.Create(),
sessionId,
userId,
userName,
DateTime.UtcNow
);
await _attendanceRepository.InsertAsync(attendance, autoSave: true);
await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString());
await Clients.Group(sessionId.ToString())
.SendAsync("ParticipantJoined", _currentUser.Id, userName);
_logger.LogInformation($"User {userName} joined class {sessionId}");
.SendAsync("ParticipantJoined", userId, userName);
}
[HubMethodName("LeaveClass")]
public async Task LeaveClassAsync(Guid sessionId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, sessionId.ToString());
var userId = _currentUser.Id;
if (userId.HasValue)
{
var attendance = await _attendanceRepository.FirstOrDefaultAsync(
x => x.SessionId == sessionId && x.StudentId == userId.Value && x.LeaveTime == null
);
if (attendance != null)
{
attendance.LeaveTime = DateTime.UtcNow;
attendance.TotalDurationMinutes = (int)Math.Max(
1,
(attendance.LeaveTime.Value - attendance.JoinTime).TotalMinutes
);
await _attendanceRepository.UpdateAsync(attendance, autoSave: true);
await Clients.Group(sessionId.ToString())
.SendAsync("AttendanceUpdated", attendance);
}
}
await Clients.Group(sessionId.ToString())
.SendAsync("ParticipantLeft", _currentUser);
_logger.LogInformation($"User {_currentUser} left class {sessionId}");
}
public async Task SendSignalingMessageAsync(SignalingMessageDto message)
{
// Forward WebRTC signaling messages
await Clients.User(message.ToUserId)
.SendAsync("ReceiveSignalingMessage", message);
_logger.LogInformation($"Signaling message sent from {message.FromUserId} to {message.ToUserId}");
}
public async Task SendChatMessageAsync(Guid sessionId, string message)
{
var userName = _currentUser.UserName;
var userId = _currentUser.Id;
// Check if user is teacher
var participant = await _participantRepository.FirstOrDefaultAsync(
x => x.SessionId == sessionId && x.UserId == userId
);
var isTeacher = participant?.IsTeacher ?? false;
// Save message to database
var chatMessage = new ClassChat(
_guidGenerator.Create(),
sessionId,
userId,
userName,
message,
isTeacher
);
await _chatMessageRepository.InsertAsync(chatMessage);
// Send to all participants
await Clients.Group(sessionId.ToString())
.SendAsync("ChatMessage", new
{
Id = chatMessage.Id,
SenderId = chatMessage.SenderId,
SenderName = chatMessage.SenderName,
Message = chatMessage.Message,
Timestamp = chatMessage.Timestamp,
IsTeacher = chatMessage.IsTeacher
});
}
public async Task MuteParticipantAsync(Guid sessionId, Guid participantId, bool isMuted)
[HubMethodName("MuteParticipant")]
public async Task MuteParticipantAsync(Guid sessionId, Guid userId, bool isMuted, bool isTeacher)
{
var teacherParticipant = await _participantRepository.FirstOrDefaultAsync(
x => x.SessionId == sessionId && x.UserId == _currentUser.Id
@ -128,59 +132,264 @@ public class ClassroomHub : Hub
}
var participant = await _participantRepository.FirstOrDefaultAsync(
x => x.SessionId == sessionId && x.UserId == participantId
x => x.SessionId == sessionId && x.UserId == userId
);
if (participant != null)
{
if (isMuted)
participant.MuteAudio();
else
participant.UnmuteAudio();
if (isMuted) participant.MuteAudio();
else participant.UnmuteAudio();
await _participantRepository.UpdateAsync(participant);
await _participantRepository.UpdateAsync(participant, autoSave: true);
// Notify the participant and others
await Clients.Group(sessionId.ToString())
.SendAsync("ParticipantMuted", participantId, isMuted);
.SendAsync("ParticipantMuted", userId, isMuted);
}
}
[HubMethodName("SendChatMessage")]
public async Task SendChatMessageAsync(
Guid sessionId,
Guid senderId,
string senderName,
string message,
bool isTeacher,
string messageType)
{
// Save message to DB
var chatMessage = new ClassroomChat(
_guidGenerator.Create(),
sessionId,
senderId,
senderName,
message,
null,
null,
isTeacher,
messageType
);
await _chatMessageRepository.InsertAsync(chatMessage, autoSave: true);
// Broadcast to group
await Clients.Group(sessionId.ToString()).SendAsync("ChatMessage", new
{
Id = chatMessage.Id,
SenderId = senderId,
SenderName = senderName,
Message = chatMessage.Message,
Timestamp = chatMessage.Timestamp,
IsTeacher = isTeacher,
MessageType = messageType
});
}
[HubMethodName("SendPrivateMessage")]
public async Task SendPrivateMessageAsync(
Guid sessionId,
Guid senderId,
string senderName,
string message,
Guid recipientId,
string recipientName,
bool isTeacher,
string messageType)
{
// Save message to DB
var chatMessage = new ClassroomChat(
_guidGenerator.Create(),
sessionId,
senderId,
senderName,
message,
recipientId,
recipientName,
isTeacher,
"private"
);
await _chatMessageRepository.InsertAsync(chatMessage, autoSave: true);
await Clients.User(recipientId.ToString()).SendAsync("ChatMessage", new
{
Id = Guid.NewGuid(),
SenderId = senderId,
SenderName = senderName,
Message = message,
Timestamp = DateTime.UtcNow,
IsTeacher = isTeacher,
RecipientId = recipientId,
RecipientName = recipientName,
MessageType = "private"
});
await Clients.Caller.SendAsync("ChatMessage", new
{
Id = Guid.NewGuid(),
SenderId = senderId,
SenderName = senderName,
Message = message,
Timestamp = DateTime.UtcNow,
IsTeacher = isTeacher,
RecipientId = recipientId,
RecipientName = recipientName,
MessageType = "private"
});
}
[HubMethodName("SendAnnouncement")]
public async Task SendAnnouncementAsync(Guid sessionId, Guid senderId, string senderName, string message, bool isTeacher)
{
// Save message to DB
var chatMessage = new ClassroomChat(
_guidGenerator.Create(),
sessionId,
senderId,
senderName,
message,
null,
null,
isTeacher,
"announcement"
);
await _chatMessageRepository.InsertAsync(chatMessage, autoSave: true);
await Clients.Group(sessionId.ToString()).SendAsync("ChatMessage", new
{
Id = Guid.NewGuid(),
SenderId = senderId,
SenderName = senderName,
Message = message,
Timestamp = DateTime.UtcNow,
IsTeacher = isTeacher,
MessageType = "announcement"
});
}
[HubMethodName("RaiseHand")]
public async Task RaiseHandAsync(Guid sessionId, Guid studentId, string studentName)
{
await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseReceived", new
{
Id = Guid.NewGuid(),
StudentId = studentId,
StudentName = studentName,
Timestamp = DateTime.UtcNow,
IsActive = true
});
}
[HubMethodName("KickParticipant")]
public async Task KickParticipantAsync(Guid sessionId, Guid participantId)
{
// Attendance kapat
var attendance = await _attendanceRepository.FirstOrDefaultAsync(
x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null
);
if (attendance != null)
{
attendance.LeaveTime = DateTime.UtcNow;
attendance.TotalDurationMinutes = (int)Math.Max(
1,
(attendance.LeaveTime.Value - attendance.JoinTime).TotalMinutes
);
await _attendanceRepository.UpdateAsync(attendance, autoSave: true);
// Katılım güncellemesini yayınla
await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", new
{
attendance.Id,
attendance.SessionId,
attendance.StudentId,
attendance.StudentName,
attendance.JoinTime,
attendance.LeaveTime,
attendance.TotalDurationMinutes
});
}
// Katılımcı çıkışını bildir
await Clients.Group(sessionId.ToString()).SendAsync("ParticipantLeft", participantId);
}
[HubMethodName("ApproveHandRaise")]
public async Task ApproveHandRaiseAsync(Guid sessionId, Guid handRaiseId)
{
await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", handRaiseId);
}
[HubMethodName("DismissHandRaise")]
public async Task DismissHandRaiseAsync(Guid sessionId, Guid handRaiseId)
{
await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", handRaiseId);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
try
{
// bağlantı gerçekten iptal edilmişse DB sorgusu çalıştırma
if (Context.ConnectionAborted.IsCancellationRequested)
return;
var userId = _currentUser.Id;
if (userId.HasValue)
{
// 🔑 1. Katılımcı listesi
var participants = await _participantRepository
.GetListAsync(x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId)
.ConfigureAwait(false);
.GetListAsync(x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId);
foreach (var participant in participants)
{
// 🔑 2. Attendance kaydını kapat
var attendance = await _attendanceRepository.FirstOrDefaultAsync(
x => x.SessionId == participant.SessionId &&
x.StudentId == userId.Value &&
x.LeaveTime == null
);
if (attendance != null)
{
attendance.LeaveTime = DateTime.UtcNow;
attendance.TotalDurationMinutes = (int)Math.Max(
1,
(attendance.LeaveTime.Value - attendance.JoinTime).TotalMinutes
);
await _attendanceRepository.UpdateAsync(attendance, autoSave: true);
// Frontende bildir
await Clients.Group(participant.SessionId.ToString())
.SendAsync("AttendanceUpdated", new
{
attendance.Id,
attendance.SessionId,
attendance.StudentId,
attendance.StudentName,
attendance.JoinTime,
attendance.LeaveTime,
attendance.TotalDurationMinutes
});
}
// 🔑 3. ParticipantLeft eventi
await Clients.Group(participant.SessionId.ToString())
.SendAsync("ParticipantLeft", userId.Value)
.ConfigureAwait(false);
.SendAsync("ParticipantLeft", userId.Value);
}
}
}
catch (TaskCanceledException)
{
// bağlantı kapandığında doğal, error yerine debug seviyesinde loglayın
_logger.LogDebug("OnDisconnectedAsync iptal edildi (connection aborted).");
}
catch (Exception ex)
{
// beklenmeyen hataları error olarak loglayın
_logger.LogError(ex, "OnDisconnectedAsync hata");
}
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
await base.OnDisconnectedAsync(exception);
}
}

View file

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

View file

@ -1,9 +1,9 @@
import { ClassAttendanceDto } from '@/proxy/classroom/models'
import { ClassroomAttendanceDto } from '@/proxy/classroom/models'
import React, { useEffect, useState } from 'react'
import { FaClock, FaUsers } from 'react-icons/fa'
interface AttendancePanelProps {
attendanceRecords: ClassAttendanceDto[]
attendanceRecords: ClassroomAttendanceDto[]
isOpen: boolean
onClose: () => void
}

View file

@ -1,10 +1,10 @@
import { ClassChatDto } from '@/proxy/classroom/models'
import { ClassroomChatDto } from '@/proxy/classroom/models'
import { useStoreState } from '@/store/store'
import React, { useState, useRef, useEffect } from 'react'
import { FaPaperPlane, FaComments, FaTimes, FaUsers, FaUser, FaBullhorn } from 'react-icons/fa'
interface ChatPanelProps {
messages: ClassChatDto[]
messages: ClassroomChatDto[]
isTeacher: boolean
isOpen: boolean
onClose: () => void

View file

@ -1,10 +1,10 @@
import React from 'react'
import { FaMicrophoneSlash, FaExpand, FaUserTimes } from 'react-icons/fa'
import { VideoPlayer } from './VideoPlayer'
import { ClassParticipantDto, VideoLayoutDto } from '@/proxy/classroom/models'
import { ClassroomParticipantDto, VideoLayoutDto } from '@/proxy/classroom/models'
interface ParticipantGridProps {
participants: ClassParticipantDto[]
participants: ClassroomParticipantDto[]
localStream?: MediaStream
currentUserId: string
currentUserName: string
@ -14,7 +14,7 @@ interface ParticipantGridProps {
onToggleAudio: () => void
onToggleVideo: () => void
onLeaveCall: () => void
onMuteParticipant?: (participantId: string, isMuted: boolean) => void
onMuteParticipant?: (participantId: string, isMuted: boolean, isTeacher: boolean) => void
layout: VideoLayoutDto
focusedParticipant?: string
onParticipantFocus?: (participantId: string | undefined) => void
@ -211,7 +211,7 @@ export const ParticipantGrid: React.FC<ParticipantGridProps> = ({
}
const renderParticipant = (
participant: ClassParticipantDto,
participant: ClassroomParticipantDto,
isMain: boolean = false,
isSmall: boolean = false,
) => (
@ -243,7 +243,7 @@ export const ParticipantGrid: React.FC<ParticipantGridProps> = ({
<button
onClick={(e) => {
e.stopPropagation()
onMuteParticipant?.(participant.id, !participant.isAudioMuted)
onMuteParticipant?.(participant.id, !participant.isAudioMuted, isTeacher)
}}
className={`p-1 rounded-full text-white text-xs ${
participant.isAudioMuted ? 'bg-red-600' : 'bg-gray-600 hover:bg-gray-700'

View file

@ -39,7 +39,7 @@ export interface ClassroomSettingsDto {
autoMuteNewParticipants: boolean
}
export interface ClassAttendanceDto {
export interface ClassroomAttendanceDto {
id: string
sessionId: string
studentId: string
@ -49,16 +49,7 @@ export interface ClassAttendanceDto {
totalDurationMinutes: number
}
export type MediaType = 'audio' | 'video' | 'screen'
export interface SignalingMessageDto {
type: MediaType
fromUserId: string
toUserId: string
data: any
}
export interface ClassParticipantDto {
export interface ClassroomParticipantDto {
id: string
name: string
isTeacher: boolean
@ -73,7 +64,7 @@ export interface ClassParticipantDto {
export type messageType = 'public' | 'private' | 'announcement'
export interface ClassChatDto {
export interface ClassroomChatDto {
id: string
senderId: string
senderName: string

View file

@ -1,20 +1,14 @@
import {
ClassAttendanceDto,
ClassChatDto,
HandRaiseDto,
SignalingMessageDto,
} from '@/proxy/classroom/models'
import { ClassroomAttendanceDto, ClassroomChatDto, HandRaiseDto } from '@/proxy/classroom/models'
import { store } from '@/store/store'
import * as signalR from '@microsoft/signalr'
export class SignalRService {
private connection!: signalR.HubConnection
private isConnected: boolean = false
private onSignalingMessage?: (message: SignalingMessageDto) => void
private onAttendanceUpdate?: (record: ClassAttendanceDto) => void
private onAttendanceUpdate?: (record: ClassroomAttendanceDto) => void
private onParticipantJoined?: (userId: string, name: string) => void
private onParticipantLeft?: (userId: string) => void
private onChatMessage?: (message: ClassChatDto) => void
private onChatMessage?: (message: ClassroomChatDto) => void
private onParticipantMuted?: (userId: string, isMuted: boolean) => void
private onHandRaiseReceived?: (handRaise: HandRaiseDto) => void
private onHandRaiseDismissed?: (handRaiseId: string) => void
@ -38,11 +32,7 @@ export class SignalRService {
private setupEventHandlers() {
if (!this.connection) return
this.connection.on('ReceiveSignalingMessage', (message: SignalingMessageDto) => {
this.onSignalingMessage?.(message)
})
this.connection.on('AttendanceUpdated', (record: ClassAttendanceDto) => {
this.connection.on('AttendanceUpdated', (record: ClassroomAttendanceDto) => {
this.onAttendanceUpdate?.(record)
})
@ -95,16 +85,21 @@ export class SignalRService {
}
}
async joinClass(sessionId: string, userName: string): Promise<void> {
async joinClass(
sessionId: string,
userId: string,
userName: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
console.log('Error starting SignalR connection join class for', userName)
// Simulate successful join in demo mode
// Don't auto-add participants in demo mode - let manual simulation handle this
return
}
console.log('Joining class session:', sessionId, 'as', userName, 'isTeacher:', isTeacher)
try {
await this.connection.invoke('JoinClass', sessionId, userName)
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher)
} catch (error) {
console.error('Error joining class:', error)
}
@ -129,21 +124,6 @@ export class SignalRService {
}
}
async sendSignalingMessage(message: SignalingMessageDto): Promise<void> {
if (!this.isConnected) {
console.log('Error starting SignalR connection 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,
@ -153,8 +133,8 @@ export class SignalRService {
): Promise<void> {
if (!this.isConnected) {
console.log('Error starting SignalR connection simulating chat message from', senderName)
const chatMessage: ClassChatDto = {
id: `msg-${Date.now()}`,
const chatMessage: ClassroomChatDto = {
id: crypto.randomUUID(),
senderId,
senderName,
message,
@ -199,8 +179,8 @@ export class SignalRService {
'to',
recipientName,
)
const chatMessage: ClassChatDto = {
id: `msg-${Date.now()}`,
const chatMessage: ClassroomChatDto = {
id: crypto.randomUUID(),
senderId,
senderName,
message,
@ -226,6 +206,7 @@ export class SignalRService {
recipientId,
recipientName,
isTeacher,
'private',
)
} catch (error) {
console.error('Error sending private message:', error)
@ -237,16 +218,17 @@ export class SignalRService {
senderId: string,
senderName: string,
message: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
console.log('Error starting SignalR connection simulating announcement from', senderName)
const chatMessage: ClassChatDto = {
id: `msg-${Date.now()}`,
const chatMessage: ClassroomChatDto = {
id: crypto.randomUUID(),
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher: true,
isTeacher,
messageType: 'announcement',
}
setTimeout(() => {
@ -256,13 +238,25 @@ export class SignalRService {
}
try {
await this.connection.invoke('SendAnnouncement', sessionId, senderId, senderName, message)
await this.connection.invoke(
'SendAnnouncement',
sessionId,
senderId,
senderName,
message,
isTeacher,
)
} catch (error) {
console.error('Error sending chat message:', error)
}
}
async muteParticipant(sessionId: string, userId: string, isMuted: boolean): Promise<void> {
async muteParticipant(
sessionId: string,
userId: string,
isMuted: boolean,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
console.log('Error starting SignalR connection simulating mute participant', userId, isMuted)
setTimeout(() => {
@ -271,8 +265,10 @@ export class SignalRService {
return
}
console.log('Muting participant:', userId, 'Muted:', isMuted, 'isTeacher:', isTeacher)
try {
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted)
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
} catch (error) {
console.error('Error muting participant:', error)
}
@ -282,7 +278,7 @@ export class SignalRService {
if (!this.isConnected) {
console.log('Error starting SignalR connection simulating hand raise from', studentName)
const handRaise: HandRaiseDto = {
id: `hand-${Date.now()}`,
id: crypto.randomUUID(),
studentId,
studentName,
timestamp: new Date().toISOString(),
@ -349,11 +345,7 @@ export class SignalRService {
}
}
setSignalingHandler(callback: (message: SignalingMessageDto) => void) {
this.onSignalingMessage = callback
}
setAttendanceUpdatedHandler(callback: (record: ClassAttendanceDto) => void) {
setAttendanceUpdatedHandler(callback: (record: ClassroomAttendanceDto) => void) {
this.onAttendanceUpdate = callback
}
@ -365,7 +357,7 @@ export class SignalRService {
this.onParticipantLeft = callback
}
setChatMessageReceivedHandler(callback: (message: ClassChatDto) => void) {
setChatMessageReceivedHandler(callback: (message: ClassroomChatDto) => void) {
this.onChatMessage = callback
}

View file

@ -21,7 +21,7 @@ export function useClassroomLogic() {
const handleCreateClass = (classData: Partial<ClassroomDto>) => {
const newClass = {
...classData,
id: `class-${Date.now()}`,
id: crypto.randomUUID(),
teacherId: '',
teacherName: '',
isActive: false,

File diff suppressed because it is too large Load diff

View file

@ -5,9 +5,13 @@ import { Role } from '@/proxy/classroom/models'
import { useStoreActions, useStoreState } from '@/store/store'
import { useNavigate } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { Helmet } from 'react-helmet'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Container } from '@/components/shared'
const Dashboard: React.FC = () => {
const navigate = useNavigate()
const { translate } = useLocalization()
const { user } = useStoreState((state) => state.auth)
const { setUser } = useStoreActions((actions) => actions.auth.user)
@ -21,56 +25,63 @@ const Dashboard: React.FC = () => {
}
return (
<div className="flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center w-full max-w-4xl"
>
<p className="text-lg sm:text-xl text-gray-600 mb-8 sm:mb-12">Lütfen rolünüzü seçin</p>
<>
<Helmet
titleTemplate="%s | Kurs Platform"
title={translate('::' + 'App.Classroom')}
defaultTitle="Kurs Platform"
></Helmet>
<Container>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center w-full max-w-4xl"
>
<p className="text-lg sm:text-xl text-gray-600 mb-8 sm:mb-12">Lütfen rolünüzü seçin</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleRoleSelect('teacher')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-blue-500"
>
<FaGraduationCap size={48} className="mx-auto text-blue-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğretmen</h2>
<p className="text-gray-600 text-sm sm:text-base">
Ders başlatın, öğrencilerle iletişim kurun ve katılım raporlarını görün
</p>
</motion.button>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleRoleSelect('teacher')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-blue-500"
>
<FaGraduationCap size={48} className="mx-auto text-blue-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğretmen</h2>
<p className="text-gray-600 text-sm sm:text-base">
Ders başlatın, öğrencilerle iletişim kurun ve katılım raporlarını görün
</p>
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleRoleSelect('student')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-green-500"
>
<FaUserCheck size={48} className="mx-auto text-green-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğrenci</h2>
<p className="text-gray-600 text-sm sm:text-base">
Aktif derslere katılın, öğretmeniniz ve diğer öğrencilerle etkileşim kurun
</p>
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleRoleSelect('student')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-green-500"
>
<FaUserCheck size={48} className="mx-auto text-green-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğrenci</h2>
<p className="text-gray-600 text-sm sm:text-base">
Aktif derslere katılın, öğretmeniniz ve diğer öğrencilerle etkileşim kurun
</p>
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleRoleSelect('observer')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-purple-500 md:col-span-2 lg:col-span-1"
>
<FaEye size={48} className="mx-auto text-purple-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Gözlemci</h2>
<p className="text-gray-600 text-sm sm:text-base">
Sınıfı gözlemleyin, eğitim sürecini takip edin (ses/video paylaşımı yok)
</p>
</motion.button>
</div>
</motion.div>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleRoleSelect('observer')}
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-purple-500 md:col-span-2 lg:col-span-1"
>
<FaEye size={48} className="mx-auto text-purple-600 mb-4 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Gözlemci</h2>
<p className="text-gray-600 text-sm sm:text-base">
Sınıfı gözlemleyin, eğitim sürecini takip edin (ses/video paylaşımı yok)
</p>
</motion.button>
</div>
</motion.div>
</Container>
</>
)
}

File diff suppressed because it is too large Load diff