online classroom SignalR
This commit is contained in:
parent
76615b074b
commit
e96faabd76
12 changed files with 102 additions and 69 deletions
|
|
@ -8,13 +8,13 @@ namespace Kurs.Platform.Classrooms;
|
||||||
|
|
||||||
public interface IClassroomAppService : IApplicationService
|
public interface IClassroomAppService : IApplicationService
|
||||||
{
|
{
|
||||||
Task<ClassroomDto> CreateAsync(ClassroomDto input);
|
|
||||||
Task<PagedResultDto<ClassroomDto>> GetListAsync(PagedAndSortedResultRequestDto input);
|
|
||||||
Task<ClassroomDto> GetAsync(Guid id);
|
Task<ClassroomDto> GetAsync(Guid id);
|
||||||
|
Task<PagedResultDto<ClassroomDto>> GetListAsync(PagedAndSortedResultRequestDto input);
|
||||||
|
Task<ClassroomDto> CreateAsync(ClassroomDto input);
|
||||||
Task<ClassroomDto> UpdateAsync(Guid id, ClassroomDto input);
|
Task<ClassroomDto> UpdateAsync(Guid id, ClassroomDto input);
|
||||||
Task DeleteAsync(Guid id);
|
Task DeleteAsync(Guid id);
|
||||||
// Task<ClassroomDto> StartClassAsync(Guid id);
|
Task<ClassroomDto> StartClassAsync(Guid id);
|
||||||
// Task EndClassAsync(Guid id);
|
Task EndClassAsync(Guid id);
|
||||||
Task<ClassroomDto> JoinClassAsync(Guid id);
|
Task<ClassroomDto> JoinClassAsync(Guid id);
|
||||||
Task LeaveClassAsync(Guid id);
|
Task LeaveClassAsync(Guid id);
|
||||||
Task<List<ClassAttendanceDto>> GetAttendanceAsync(Guid sessionId);
|
Task<List<ClassAttendanceDto>> GetAttendanceAsync(Guid sessionId);
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
public async Task<ClassroomDto> StartClassroomAsync(Guid id)
|
public async Task<ClassroomDto> StartClassAsync(Guid id)
|
||||||
{
|
{
|
||||||
var classSession = await _classSessionRepository.GetAsync(id);
|
var classSession = await _classSessionRepository.GetAsync(id);
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
public async Task EndClassroomAsync(Guid id)
|
public async Task EndClassAsync(Guid id)
|
||||||
{
|
{
|
||||||
var classSession = await _classSessionRepository.GetAsync(id);
|
var classSession = await _classSessionRepository.GetAsync(id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ namespace Kurs.Platform.Entities;
|
||||||
public class ClassChat : FullAuditedEntity<Guid>
|
public class ClassChat : FullAuditedEntity<Guid>
|
||||||
{
|
{
|
||||||
public Guid SessionId { get; set; }
|
public Guid SessionId { get; set; }
|
||||||
public Guid SenderId { get; set; }
|
public Guid? SenderId { get; set; }
|
||||||
public string SenderName { get; set; }
|
public string SenderName { get; set; }
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
|
|
@ -22,7 +22,7 @@ public class ClassChat : FullAuditedEntity<Guid>
|
||||||
public ClassChat(
|
public ClassChat(
|
||||||
Guid id,
|
Guid id,
|
||||||
Guid sessionId,
|
Guid sessionId,
|
||||||
Guid senderId,
|
Guid? senderId,
|
||||||
string senderName,
|
string senderName,
|
||||||
string message,
|
string message,
|
||||||
bool isTeacher
|
bool isTeacher
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using Volo.Abp.Domain.Repositories;
|
||||||
using Kurs.Platform.Entities;
|
using Kurs.Platform.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Volo.Abp.Guids;
|
using Volo.Abp.Guids;
|
||||||
|
using Volo.Abp.Users;
|
||||||
|
|
||||||
namespace Kurs.Platform.SignalR.Hubs;
|
namespace Kurs.Platform.SignalR.Hubs;
|
||||||
|
|
||||||
|
|
@ -17,21 +18,25 @@ public class ClassroomHub : Hub
|
||||||
private readonly IRepository<ClassChat, Guid> _chatMessageRepository;
|
private readonly IRepository<ClassChat, Guid> _chatMessageRepository;
|
||||||
private readonly ILogger<ClassroomHub> _logger;
|
private readonly ILogger<ClassroomHub> _logger;
|
||||||
private readonly IGuidGenerator _guidGenerator;
|
private readonly IGuidGenerator _guidGenerator;
|
||||||
|
private readonly ICurrentUser _currentUser;
|
||||||
|
|
||||||
public ClassroomHub(
|
public ClassroomHub(
|
||||||
IRepository<Classroom, Guid> classSessionRepository,
|
IRepository<Classroom, Guid> classSessionRepository,
|
||||||
IRepository<ClassParticipant, Guid> participantRepository,
|
IRepository<ClassParticipant, Guid> participantRepository,
|
||||||
IRepository<ClassChat, Guid> chatMessageRepository,
|
IRepository<ClassChat, Guid> chatMessageRepository,
|
||||||
ILogger<ClassroomHub> logger,
|
ILogger<ClassroomHub> logger,
|
||||||
IGuidGenerator guidGenerator)
|
IGuidGenerator guidGenerator,
|
||||||
|
ICurrentUser currentUser)
|
||||||
{
|
{
|
||||||
_classSessionRepository = classSessionRepository;
|
_classSessionRepository = classSessionRepository;
|
||||||
_participantRepository = participantRepository;
|
_participantRepository = participantRepository;
|
||||||
_chatMessageRepository = chatMessageRepository;
|
_chatMessageRepository = chatMessageRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_guidGenerator = guidGenerator;
|
_guidGenerator = guidGenerator;
|
||||||
|
_currentUser = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HubMethodName("JoinClass")]
|
||||||
public async Task JoinClassAsync(Guid sessionId, string userName)
|
public async Task JoinClassAsync(Guid sessionId, string userName)
|
||||||
{
|
{
|
||||||
var classSession = await _classSessionRepository.GetAsync(sessionId);
|
var classSession = await _classSessionRepository.GetAsync(sessionId);
|
||||||
|
|
@ -47,7 +52,7 @@ public class ClassroomHub : Hub
|
||||||
|
|
||||||
// Update participant connection
|
// Update participant connection
|
||||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||||
x => x.SessionId == sessionId && x.UserId == Context.UserIdentifier.To<Guid>()
|
x => x.SessionId == sessionId && x.UserId == _currentUser.Id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (participant != null)
|
if (participant != null)
|
||||||
|
|
@ -58,16 +63,17 @@ public class ClassroomHub : Hub
|
||||||
|
|
||||||
// Notify others
|
// Notify others
|
||||||
await Clients.Group(sessionId.ToString())
|
await Clients.Group(sessionId.ToString())
|
||||||
.SendAsync("ParticipantJoined", Context.UserIdentifier, userName);
|
.SendAsync("ParticipantJoined", _currentUser.Id, userName);
|
||||||
_logger.LogInformation($"User {userName} joined class {sessionId}");
|
_logger.LogInformation($"User {userName} joined class {sessionId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HubMethodName("LeaveClass")]
|
||||||
public async Task LeaveClassAsync(Guid sessionId)
|
public async Task LeaveClassAsync(Guid sessionId)
|
||||||
{
|
{
|
||||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, sessionId.ToString());
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, sessionId.ToString());
|
||||||
await Clients.Group(sessionId.ToString())
|
await Clients.Group(sessionId.ToString())
|
||||||
.SendAsync("ParticipantLeft", Context.UserIdentifier);
|
.SendAsync("ParticipantLeft", _currentUser);
|
||||||
_logger.LogInformation($"User {Context.UserIdentifier} left class {sessionId}");
|
_logger.LogInformation($"User {_currentUser} left class {sessionId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendSignalingMessageAsync(SignalingMessageDto message)
|
public async Task SendSignalingMessageAsync(SignalingMessageDto message)
|
||||||
|
|
@ -80,8 +86,8 @@ public class ClassroomHub : Hub
|
||||||
|
|
||||||
public async Task SendChatMessageAsync(Guid sessionId, string message)
|
public async Task SendChatMessageAsync(Guid sessionId, string message)
|
||||||
{
|
{
|
||||||
var userId = Context.UserIdentifier.To<Guid>();
|
var userName = _currentUser.UserName;
|
||||||
var userName = Context.User?.Identity?.Name ?? "Unknown";
|
var userId = _currentUser.Id;
|
||||||
|
|
||||||
// Check if user is teacher
|
// Check if user is teacher
|
||||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||||
|
|
@ -118,7 +124,7 @@ public class ClassroomHub : Hub
|
||||||
public async Task MuteParticipantAsync(Guid sessionId, Guid participantId, bool isMuted)
|
public async Task MuteParticipantAsync(Guid sessionId, Guid participantId, bool isMuted)
|
||||||
{
|
{
|
||||||
var teacherParticipant = await _participantRepository.FirstOrDefaultAsync(
|
var teacherParticipant = await _participantRepository.FirstOrDefaultAsync(
|
||||||
x => x.SessionId == sessionId && x.UserId == Context.UserIdentifier.To<Guid>()
|
x => x.SessionId == sessionId && x.UserId == _currentUser.Id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (teacherParticipant?.IsTeacher != true)
|
if (teacherParticipant?.IsTeacher != true)
|
||||||
|
|
@ -149,7 +155,7 @@ public class ClassroomHub : Hub
|
||||||
public override async Task OnDisconnectedAsync(Exception exception)
|
public override async Task OnDisconnectedAsync(Exception exception)
|
||||||
{
|
{
|
||||||
// Handle cleanup when user disconnects
|
// Handle cleanup when user disconnects
|
||||||
var userId = Context.UserIdentifier?.To<Guid>();
|
var userId = _currentUser.Id;
|
||||||
if (userId.HasValue)
|
if (userId.HasValue)
|
||||||
{
|
{
|
||||||
var participants = await _participantRepository.GetListAsync(
|
var participants = await _participantRepository.GetListAsync(
|
||||||
|
|
@ -11,6 +11,7 @@ using Kurs.Platform.BlobStoring;
|
||||||
using Kurs.Platform.EntityFrameworkCore;
|
using Kurs.Platform.EntityFrameworkCore;
|
||||||
using Kurs.Platform.Extensions;
|
using Kurs.Platform.Extensions;
|
||||||
using Kurs.Platform.Identity;
|
using Kurs.Platform.Identity;
|
||||||
|
using Kurs.Platform.SignalR.Hubs;
|
||||||
using Kurs.Settings;
|
using Kurs.Settings;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
|
@ -112,6 +113,8 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
ConfigureHangfire(context, configuration);
|
ConfigureHangfire(context, configuration);
|
||||||
ConfigureBlobStoring(configuration);
|
ConfigureBlobStoring(configuration);
|
||||||
|
|
||||||
|
context.Services.AddSignalR();
|
||||||
|
|
||||||
Configure<AbpExceptionHttpStatusCodeOptions>(options =>
|
Configure<AbpExceptionHttpStatusCodeOptions>(options =>
|
||||||
{
|
{
|
||||||
options.Map(AppErrorCodes.NoAuth, System.Net.HttpStatusCode.Unauthorized);
|
options.Map(AppErrorCodes.NoAuth, System.Net.HttpStatusCode.Unauthorized);
|
||||||
|
|
@ -329,7 +332,7 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
fileSystem.BasePath = configuration["App:CdnPath"];
|
fileSystem.BasePath = configuration["App:CdnPath"];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
options.Containers.Configure<ImportBlobContainer>(container =>
|
options.Containers.Configure<ImportBlobContainer>(container =>
|
||||||
{
|
{
|
||||||
container.UseFileSystem(fileSystem =>
|
container.UseFileSystem(fileSystem =>
|
||||||
|
|
@ -400,6 +403,11 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
AsyncAuthorization = [new AbpHangfireAuthorizationFilter()]
|
AsyncAuthorization = [new AbpHangfireAuthorizationFilter()]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapHub<ClassroomHub>("/classroomhub");
|
||||||
|
});
|
||||||
|
|
||||||
app.UseConfiguredEndpoints();
|
app.UseConfiguredEndpoints();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export interface User {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClassroomDto {
|
export interface ClassroomDto {
|
||||||
id?: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
subject?: string
|
subject?: string
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export const ROUTES_ENUM = {
|
||||||
classroom: {
|
classroom: {
|
||||||
dashboard: '/admin/classroom/dashboard',
|
dashboard: '/admin/classroom/dashboard',
|
||||||
classes: '/admin/classroom/classes',
|
classes: '/admin/classroom/classes',
|
||||||
classroom: '/admin/classroom/room/:id',
|
roomDetail: '/admin/classroom/room/:id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
accessDenied: '/admin/access-denied',
|
accessDenied: '/admin/access-denied',
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ import { PagedAndSortedResultRequestDto, PagedResultDto } from '@/proxy'
|
||||||
export const getClassroomById = (id: string) =>
|
export const getClassroomById = (id: string) =>
|
||||||
apiService.fetchData<ClassroomDto>({
|
apiService.fetchData<ClassroomDto>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `/api/app/classroom`,
|
url: `/api/app/classroom/${id}`,
|
||||||
params: { id },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getClassrooms = (input: PagedAndSortedResultRequestDto) =>
|
export const getClassrooms = (input: PagedAndSortedResultRequestDto) =>
|
||||||
|
|
@ -39,11 +38,11 @@ export const deleteClassroom = (id: string) =>
|
||||||
export const startClassroom = (id: string) =>
|
export const startClassroom = (id: string) =>
|
||||||
apiService.fetchData({
|
apiService.fetchData({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: `/api/app/${id}/start-classroom`,
|
url: `/api/app/classroom/${id}/start-class`,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const endClassroom = (id: string) =>
|
export const endClassroom = (id: string) =>
|
||||||
apiService.fetchData({
|
apiService.fetchData({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: `/api/app/${id}/end-classroom`,
|
url: `/api/app/classroom/${id}/end-class`,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
HandRaiseDto,
|
HandRaiseDto,
|
||||||
SignalingMessageDto,
|
SignalingMessageDto,
|
||||||
} from '@/proxy/classroom/models'
|
} from '@/proxy/classroom/models'
|
||||||
|
import { store } from '@/store/store'
|
||||||
import * as signalR from '@microsoft/signalr'
|
import * as signalR from '@microsoft/signalr'
|
||||||
|
|
||||||
export class SignalRService {
|
export class SignalRService {
|
||||||
|
|
@ -20,15 +21,17 @@ export class SignalRService {
|
||||||
private onHandRaiseDismissed?: (handRaiseId: string) => void
|
private onHandRaiseDismissed?: (handRaiseId: string) => void
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
const { auth } = store.getState()
|
||||||
|
|
||||||
// Only initialize connection if not in demo mode
|
// Only initialize connection if not in demo mode
|
||||||
if (!this.demoMode) {
|
if (!this.demoMode) {
|
||||||
// In production, replace with your actual SignalR hub URL
|
// In production, replace with your actual SignalR hub URL
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
this.connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl('https://localhost:5001/classroomhub', {
|
.withUrl('https://localhost:44344/classroomhub', {
|
||||||
skipNegotiation: true,
|
accessTokenFactory: () => auth.session.token || '',
|
||||||
transport: signalR.HttpTransportType.WebSockets,
|
|
||||||
})
|
})
|
||||||
.withAutomaticReconnect()
|
.withAutomaticReconnect()
|
||||||
|
.configureLogging(signalR.LogLevel.Information)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
this.setupEventHandlers()
|
this.setupEventHandlers()
|
||||||
|
|
@ -77,6 +80,10 @@ export class SignalRService {
|
||||||
this.connection.onclose(() => {
|
this.connection.onclose(() => {
|
||||||
console.log('SignalR connection closed')
|
console.log('SignalR connection closed')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.connection.on('Error', (message: string) => {
|
||||||
|
console.error('Hub error:', message)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
|
|
@ -98,7 +105,7 @@ export class SignalRService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinClass(sessionId: string, userId: string, userName: string): Promise<void> {
|
async joinClass(sessionId: string, userName: string): Promise<void> {
|
||||||
if (this.demoMode || !this.isConnected) {
|
if (this.demoMode || !this.isConnected) {
|
||||||
console.log('Demo mode: Simulating join class for', userName)
|
console.log('Demo mode: Simulating join class for', userName)
|
||||||
// Simulate successful join in demo mode
|
// Simulate successful join in demo mode
|
||||||
|
|
@ -107,24 +114,26 @@ export class SignalRService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.connection.invoke('JoinClass', sessionId, userId, userName)
|
await this.connection.invoke('JoinClass', sessionId, userName)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error joining class:', error)
|
console.error('Error joining class:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async leaveClass(sessionId: string, userId: string): Promise<void> {
|
async leaveClass(sessionId: string): Promise<void> {
|
||||||
|
const { auth } = store.getState()
|
||||||
|
|
||||||
if (this.demoMode || !this.isConnected) {
|
if (this.demoMode || !this.isConnected) {
|
||||||
console.log('Demo mode: Simulating leave class for user', userId)
|
console.log('Demo mode: Simulating leave class for user', auth.user.id)
|
||||||
// Simulate successful leave in demo mode
|
// Simulate successful leave in demo mode
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.onParticipantLeft?.(userId)
|
this.onParticipantLeft?.(auth.user.id)
|
||||||
}, 100)
|
}, 100)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.connection.invoke('LeaveClass', sessionId, userId)
|
await this.connection.invoke('LeaveClass', sessionId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error leaving class:', error)
|
console.error('Error leaving class:', error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ platformApiService.interceptors.response.use(
|
||||||
email: tokenDetails?.email,
|
email: tokenDetails?.email,
|
||||||
authority: [tokenDetails?.role],
|
authority: [tokenDetails?.role],
|
||||||
name: `${tokenDetails?.given_name} ${tokenDetails?.family_name}`.trim(),
|
name: `${tokenDetails?.given_name} ${tokenDetails?.family_name}`.trim(),
|
||||||
|
role: 'teacher',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
setIsRefreshing(false)
|
setIsRefreshing(false)
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ const ClassList: React.FC = () => {
|
||||||
setClassroom(newClassEntity)
|
setClassroom(newClassEntity)
|
||||||
|
|
||||||
if (classroom.id) {
|
if (classroom.id) {
|
||||||
navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', classroom.id))
|
handleJoinClass(classroom)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Sınıf oluştururken hata oluştu:', error)
|
console.error('Sınıf oluştururken hata oluştu:', error)
|
||||||
|
|
@ -152,8 +152,12 @@ const ClassList: React.FC = () => {
|
||||||
await startClassroom(classSession.id!)
|
await startClassroom(classSession.id!)
|
||||||
getClassroomList()
|
getClassroomList()
|
||||||
|
|
||||||
|
handleJoinClass(classSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleJoinClass = (classSession: ClassroomDto) => {
|
||||||
if (classSession.id) {
|
if (classSession.id) {
|
||||||
navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', classSession.id))
|
navigate(ROUTES_ENUM.protected.admin.classroom.roomDetail.replace(':id', classSession.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,16 +317,21 @@ const ClassList: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
onClick={() => openEditModal(classSession)}
|
onClick={() => openEditModal(classSession)}
|
||||||
disabled={classSession.isActive}
|
disabled={classSession.isActive}
|
||||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700"
|
className="flex px-3 sm:px-4 py-2 rounded-lg bg-blue-600 text-white
|
||||||
|
hover:bg-blue-700
|
||||||
|
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
||||||
title="Sınıfı Düzenle"
|
title="Sınıfı Düzenle"
|
||||||
>
|
>
|
||||||
<FaEdit size={14} />
|
<FaEdit size={14} />
|
||||||
Düzenle
|
Düzenle
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => openDeleteModal(classSession)}
|
onClick={() => openDeleteModal(classSession)}
|
||||||
disabled={classSession.isActive}
|
disabled={classSession.isActive}
|
||||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700"
|
className="flex px-3 sm:px-4 py-2 rounded-lg bg-red-600 text-white
|
||||||
|
hover:bg-red-700
|
||||||
|
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
||||||
title="Sınıfı Sil"
|
title="Sınıfı Sil"
|
||||||
>
|
>
|
||||||
<FaTrash size={14} />
|
<FaTrash size={14} />
|
||||||
|
|
@ -334,16 +343,10 @@ const ClassList: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
user.role === 'teacher' && classSession.teacherId === user.id
|
user.role === 'teacher' && classSession.teacherId === user.id
|
||||||
? handleStartClass(classSession)
|
? classSession.isActive
|
||||||
: (() => {
|
? handleJoinClass(classSession)
|
||||||
if (classSession.id)
|
: handleStartClass(classSession)
|
||||||
navigate(
|
: handleJoinClass(classSession)
|
||||||
ROUTES_ENUM.protected.admin.classroom.classroom.replace(
|
|
||||||
':id',
|
|
||||||
classSession.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})()
|
|
||||||
}
|
}
|
||||||
className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${
|
className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${
|
||||||
user.role === 'teacher' && classSession.teacherId === user.id
|
user.role === 'teacher' && classSession.teacherId === user.id
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ const newClassSession: ClassroomDto = {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
isScheduled: false,
|
isScheduled: false,
|
||||||
participantCount: 0,
|
participantCount: 0,
|
||||||
settings: undefined,
|
settingsDto: undefined,
|
||||||
canJoin: false,
|
canJoin: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,28 +157,42 @@ const RoomDetail: React.FC = () => {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchClassDetails = async () => {
|
||||||
initializeServices()
|
const classEntity = await getClassroomById(params?.id ?? '')
|
||||||
return () => {
|
if (classEntity) {
|
||||||
cleanup()
|
classEntity.data.scheduledStartTime = showDbDateAsIs(classEntity.data.scheduledStartTime)
|
||||||
|
setClassSession(classEntity.data)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchClassDetails()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (classSession.id) {
|
||||||
|
initializeServices()
|
||||||
|
return () => {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [classSession.id])
|
||||||
|
|
||||||
// Apply class settings
|
// Apply class settings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (classSession?.settings) {
|
if (classSession?.settingsDto) {
|
||||||
setClassSettings(classSession.settings)
|
setClassSettings(classSession.settingsDto)
|
||||||
const selectedLayout =
|
const selectedLayout =
|
||||||
layouts.find((l) => l.id === classSession.settings!.defaultLayout) || layouts[0]
|
layouts.find((l) => l.id === classSession.settingsDto!.defaultLayout) || layouts[0]
|
||||||
setCurrentLayout(selectedLayout)
|
setCurrentLayout(selectedLayout)
|
||||||
|
|
||||||
// Apply default audio/video states for new participants
|
// Apply default audio/video states for new participants
|
||||||
if (user.role === 'student') {
|
if (user.role === 'student') {
|
||||||
setIsAudioEnabled(classSession.settings.defaultMicrophoneState === 'unmuted')
|
setIsAudioEnabled(classSession.settingsDto.defaultMicrophoneState === 'unmuted')
|
||||||
setIsVideoEnabled(classSession.settings.defaultCameraState === 'on')
|
setIsVideoEnabled(classSession.settingsDto.defaultCameraState === 'on')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [classSession?.settings, user.role])
|
}, [classSession?.settingsDto, user.role])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
@ -190,13 +204,6 @@ const RoomDetail: React.FC = () => {
|
||||||
|
|
||||||
const initializeServices = async () => {
|
const initializeServices = async () => {
|
||||||
try {
|
try {
|
||||||
//ClassEntity
|
|
||||||
const classEntity = await getClassroomById(params?.id ?? '')
|
|
||||||
if (classEntity) {
|
|
||||||
classEntity.data.scheduledStartTime = showDbDateAsIs(classEntity.data.scheduledStartTime)
|
|
||||||
setClassSession(classEntity.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize SignalR
|
// Initialize SignalR
|
||||||
signalRServiceRef.current = new SignalRService()
|
signalRServiceRef.current = new SignalRService()
|
||||||
await signalRServiceRef.current.start()
|
await signalRServiceRef.current.start()
|
||||||
|
|
@ -275,7 +282,7 @@ const RoomDetail: React.FC = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Join the class
|
// Join the class
|
||||||
await signalRServiceRef.current.joinClass(classSession.id, user.id, user.name)
|
await signalRServiceRef.current.joinClass(classSession.id, user.name)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize services:', error)
|
console.error('Failed to initialize services:', error)
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +290,7 @@ const RoomDetail: React.FC = () => {
|
||||||
|
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
if (signalRServiceRef.current) {
|
if (signalRServiceRef.current) {
|
||||||
await signalRServiceRef.current.leaveClass(classSession.id, user.id)
|
await signalRServiceRef.current.leaveClass(classSession.id)
|
||||||
await signalRServiceRef.current.disconnect()
|
await signalRServiceRef.current.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -518,9 +525,9 @@ const RoomDetail: React.FC = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add attendance record
|
// Add attendance record
|
||||||
setAttendanceRecords((prev) => {
|
setAttendanceRecords((prev: any) => {
|
||||||
// Check if student already has an active attendance record
|
// Check if student already has an active attendance record
|
||||||
const existingRecord = prev.find((r) => r.studentId === studentId && !r.leaveTime)
|
const existingRecord = prev.find((r: any) => r.studentId === studentId && !r.leaveTime)
|
||||||
if (existingRecord) return prev
|
if (existingRecord) return prev
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue