This commit is contained in:
chamikaJ
2025-04-17 18:28:54 +05:30
parent f583291d8a
commit 8825b0410a
2837 changed files with 241385 additions and 127578 deletions

View File

@@ -0,0 +1,25 @@
// src/services/notification/notificationMiddleware.ts
import { Middleware } from '@reduxjs/toolkit';
import { showAlert, hideAlert } from './alertSlice';
import alertService from './alertService';
export const notificationMiddleware: Middleware = store => next => action => {
if (showAlert.match(action)) {
const notification: any = action.payload;
// Show notification using service
alertService.error(notification.title, notification.message, notification.duration);
// Auto-remove notification after duration
if (notification.duration !== 0) {
setTimeout(
() => {
store.dispatch(hideAlert(notification.id));
},
(notification.duration || store.getState().notification.config.duration) * 1000
);
}
}
return next(action);
};

View File

@@ -0,0 +1,67 @@
import { AlertType } from '@/types/alert.types';
import DOMPurify from 'dompurify';
import { notification } from 'antd';
class AlertService {
private static instance: AlertService;
private activeAlerts: Set<string> = new Set();
private constructor() {}
public static getInstance(): AlertService {
if (!AlertService.instance) {
AlertService.instance = new AlertService();
}
return AlertService.instance;
}
private sanitizeHtml(content: string): string {
return DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'target'],
});
}
private show(type: AlertType, title: string, message: string, duration?: number): void {
if (this.activeAlerts.has(message)) return;
const safeTitle = this.sanitizeHtml(title);
const safeMessage = this.sanitizeHtml(message);
this.activeAlerts.add(message);
notification[type]({
message: safeTitle,
description: safeMessage,
duration: duration || 5,
placement: 'topRight',
style: { borderRadius: '4px' },
onClose: () => {
this.activeAlerts.delete(message);
},
});
}
public success(title: string, message: string, duration?: number): void {
this.show('success', title, message, duration);
}
public error(title: string, message: string, duration?: number): void {
this.show('error', title, message, duration);
}
public info(title: string, message: string, duration?: number): void {
this.show('info', title, message, duration);
}
public warning(title: string, message: string, duration?: number): void {
this.show('warning', title, message, duration);
}
public clearAll(): void {
notification.destroy();
this.activeAlerts.clear();
}
}
export const alertService = AlertService.getInstance();
export default alertService;

View File

@@ -0,0 +1,43 @@
import { AlertConfig, AlertState, AlertType } from '@/types/alert.types';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { notification } from 'antd';
import DOMPurify from 'dompurify';
const initialState: AlertState = {
activeAlerts: new Set(),
config: {
position: 'topRight',
duration: 4.5,
maxCount: 5,
},
};
const alertSlice = createSlice({
name: 'alert',
initialState,
reducers: {
showAlert: (
state,
action: PayloadAction<{
type: AlertType;
title: string;
message: string;
duration?: number;
}>
) => {
if (!state.activeAlerts.has(action.payload.message)) {
state.activeAlerts.add(action.payload.message);
}
},
hideAlert: (state, action: PayloadAction<string>) => {
state.activeAlerts.delete(action.payload);
},
updateAlertConfig: (state, action: PayloadAction<Partial<AlertConfig>>) => {
state.config = { ...state.config, ...action.payload };
},
},
});
export const { showAlert, hideAlert, updateAlertConfig } = alertSlice.actions;
export default alertSlice.reducer;

View File

@@ -0,0 +1,69 @@
import { ILocalSession } from '@/types/auth/local-session.types';
import logger from '@/utils/errorLogger';
import { deleteSession, getUserSession, hasSession, setSession } from '@/utils/session-helper';
import { NavigateFunction } from 'react-router-dom';
class AuthService {
private readonly navigate: NavigateFunction;
constructor(navigate: NavigateFunction) {
this.navigate = navigate;
}
// Computed property for user role
get role(): string {
const user = this.getCurrentSession();
if (!user) return 'Unknown';
if (user.owner) return 'Owner';
if (user.is_admin) return 'Admin';
return 'Member';
}
// Session management methods
public isAuthenticated(): boolean {
return !!this.getCurrentSession();
}
public isExpired(): boolean {
return !!this.getCurrentSession()?.is_expired;
}
public setCurrentSession(user: ILocalSession): void {
setSession(user);
}
public getCurrentSession(): ILocalSession | null {
return getUserSession();
}
public isOwnerOrAdmin(): boolean {
return !!(this.getCurrentSession()?.owner || this.getCurrentSession()?.is_admin);
}
// Sign out methods
public async signOut(): Promise<void> {
try {
if (hasSession()) {
deleteSession();
}
} catch (e) {
logger.error('Error signing out', e);
}
}
public hasCompletedSetup(): boolean {
const user = this.getCurrentSession();
return !!user?.setup_completed;
}
private onSignOutConfirm(): void {
void this.signOut();
window.location.href = '/secure/logout';
}
}
// Hook for using AuthService in components
export const createAuthService = (navigate: NavigateFunction): AuthService => {
return new AuthService(navigate);
};

View File

@@ -0,0 +1,154 @@
import { Message, User } from '@/types/socket.types';
import logger from '@/utils/errorLogger';
import { Socket } from 'socket.io-client';
export class SocketService {
private socket: Socket | null = null;
private static instance: SocketService | null = null;
private constructor() {}
public static getInstance(): SocketService {
if (!SocketService.instance) {
SocketService.instance = new SocketService();
}
return SocketService.instance;
}
public init(socket: Socket): void {
if (!this.socket) {
this.socket = socket;
this.setupDefaultListeners();
}
}
private setupDefaultListeners(): void {
if (!this.socket) return;
this.socket.on('connect', () => {
logger.info('Socket connected in service');
});
this.socket.on('disconnect', () => {
logger.info('Socket disconnected in service');
});
this.socket.on('error', (error: Error) => {
logger.error('Socket error', { error });
});
}
// Message Methods
public sendMessage(message: Omit<Message, 'id' | 'timestamp'>): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not initialized'));
return;
}
this.socket.emit(
'send_message',
message,
(response: { success: boolean; error?: string }) => {
if (response.success) {
resolve();
} else {
reject(new Error(response.error || 'Failed to send message'));
}
}
);
});
}
public onMessage(callback: (message: Message) => void): () => void {
if (!this.socket) throw new Error('Socket not initialized');
this.socket.on('message', callback);
return () => this.socket?.off('message', callback);
}
// User Methods
public updateUserStatus(status: User['status']): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not initialized'));
return;
}
this.socket.emit('update_status', { status }, (response: { success: boolean }) => {
if (response.success) {
resolve();
} else {
reject(new Error('Failed to update status'));
}
});
});
}
public onUserStatusChange(callback: (user: User) => void): () => void {
if (!this.socket) throw new Error('Socket not initialized');
this.socket.on('user_status_changed', callback);
return () => this.socket?.off('user_status_changed', callback);
}
// Room Methods
public joinRoom(roomId: string): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not initialized'));
return;
}
this.socket.emit('join_room', { roomId }, (response: { success: boolean }) => {
if (response.success) {
resolve();
} else {
reject(new Error('Failed to join room'));
}
});
});
}
public leaveRoom(roomId: string): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not initialized'));
return;
}
this.socket.emit('leave_room', { roomId }, (response: { success: boolean }) => {
if (response.success) {
resolve();
} else {
reject(new Error('Failed to leave room'));
}
});
});
}
// Custom Event Handler
public on<T>(event: string, callback: (data: T) => void): () => void {
if (!this.socket) throw new Error('Socket not initialized');
this.socket.on(event, callback);
return () => this.socket?.off(event, callback);
}
public emit<T>(event: string, data: T): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not initialized'));
return;
}
this.socket.emit(event, data, (response: { success: boolean; error?: string }) => {
if (response.success) {
resolve();
} else {
reject(new Error(response.error || 'Emission failed'));
}
});
});
}
}

View File

@@ -0,0 +1,40 @@
import { getCurrentGroupBoard } from '@/features/board/board-slice';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { IGroupByOption } from '@/types/tasks/taskList.types';
import { NavigateFunction } from 'react-router-dom';
const GROUP_BY_STATUS_VALUE = 'status';
const GROUP_BY_PRIORITY_VALUE = 'priority';
const GROUP_BY_PHASE_VALUE = 'phase';
const GROUP_BY_OPTIONS: IGroupByOption[] = [
{ label: 'Status', value: GROUP_BY_STATUS_VALUE },
{ label: 'Priority', value: GROUP_BY_PRIORITY_VALUE },
{ label: 'Phase', value: GROUP_BY_PHASE_VALUE },
];
class TaskListService {
private readonly navigate: NavigateFunction;
constructor(navigate: NavigateFunction) {
this.navigate = navigate;
}
}
// Hook for using AuthService in components
export const createTaskListService = (navigate: NavigateFunction): TaskListService => {
return new TaskListService(navigate);
};
export const getGroupIdByGroupedColumn = (task: IProjectTask) => {
const groupBy = getCurrentGroupBoard().value;
if (groupBy === GROUP_BY_STATUS_VALUE)
return task.status as string;
if (groupBy === GROUP_BY_PRIORITY_VALUE)
return task.priority as string;
if (groupBy === GROUP_BY_PHASE_VALUE)
return task.phase_id as string;
return null;
}