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,30 @@
// src/hooks/useAlert.ts
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { alertService } from '@/services/alerts/alertService';
import { showAlert } from '@/services/alerts/alertSlice';
import { AlertType } from '@/types/alert.types';
export const useAlert = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const show = (type: AlertType, title: string, message: string, duration?: number) => {
// Update Redux state
dispatch(showAlert({ type, title, message, duration }));
// Show alert via service
alertService[type](title, message, duration);
};
return {
success: (title: string, message: string, duration?: number) =>
show('success', title, message, duration),
error: (title: string, message: string, duration?: number) =>
show('error', title, message, duration),
info: (title: string, message: string, duration?: number) =>
show('info', title, message, duration),
warning: (title: string, message: string, duration?: number) =>
show('warning', title, message, duration),
clearAll: alertService.clearAll,
};
};

View File

@@ -0,0 +1,4 @@
import { useDispatch } from 'react-redux';
import { AppDispatch } from '../app/store';
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();

View File

@@ -0,0 +1,4 @@
import { useSelector } from 'react-redux';
import { RootState } from '../app/store';
export const useAppSelector = useSelector.withTypes<RootState>();

View File

@@ -0,0 +1,9 @@
import { createAuthService } from '@/services/auth/auth.service';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
export const useAuthService = () => {
const navigate = useNavigate();
const authService = useMemo(() => createAuthService(navigate), [navigate]);
return authService;
};

View File

@@ -0,0 +1,7 @@
import { useEffect } from 'react';
export const useDocumentTitle = (title: string) => {
return useEffect(() => {
document.title = `Worklenz | ${title}`;
}, [title]);
};

View File

@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
/**
* Custom hook to handle cursor style changes during drag operations
* @param isDragging - Boolean indicating if an item is being dragged
*/
const useDragCursor = (isDragging: boolean) => {
useIsomorphicLayoutEffect(() => {
if (!isDragging) return;
// Save the original cursor style
const originalCursor = document.body.style.cursor;
// Apply grabbing cursor to the entire document when dragging
document.body.style.cursor = 'grabbing';
// Reset cursor when dragging stops or component unmounts
return () => {
document.body.style.cursor = originalCursor;
};
}, [isDragging]);
};
export default useDragCursor;

View File

@@ -0,0 +1,16 @@
import { useAuthService } from '@/hooks/useAuth';
import { useAppSelector } from '@/hooks/useAppSelector';
const useIsProjectManager = () => {
const currentSession = useAuthService().getCurrentSession();
const { project: currentProject } = useAppSelector(state => state.projectReducer);
const { project: drawerProject } = useAppSelector(state => state.projectDrawerReducer);
// Check if user is project manager for either the current project or drawer project
const isManagerOfCurrentProject = currentSession?.team_member_id === currentProject?.project_manager?.id;
const isManagerOfDrawerProject = currentSession?.team_member_id === drawerProject?.project_manager?.id;
return isManagerOfCurrentProject || isManagerOfDrawerProject;
};
export default useIsProjectManager;

View File

@@ -0,0 +1,6 @@
import { useLayoutEffect, useEffect } from 'react';
// Use useLayoutEffect in browser environments and useEffect in SSR environments
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;

View File

@@ -0,0 +1,63 @@
import mixpanel, { Dict } from 'mixpanel-browser';
import { useCallback, useEffect, useMemo } from 'react';
import { useAuthService } from './useAuth';
import { initMixpanel } from '@/utils/mixpanelInit';
import logger from '@/utils/errorLogger';
export const useMixpanelTracking = () => {
const auth = useAuthService();
const token = useMemo(() => {
const host = window.location.host;
if (host === 'uat.worklenz.com' || host === 'dev.worklenz.com' || host === 'api.worklenz.com') {
return import.meta.env.VITE_MIXPANEL_TOKEN;
}
if (host === 'app.worklenz.com' || host === 'v2.worklenz.com') {
return import.meta.env.VITE_MIXPANEL_TOKEN;
}
return import.meta.env.VITE_MIXPANEL_TOKEN;
}, []);
useEffect(() => {
initMixpanel(token);
}, [token]);
const setIdentity = useCallback((user: any) => {
if (user?.id) {
mixpanel.identify(user.id);
mixpanel.people.set({
$user_id: user.id,
$name: user.name,
$email: user.email,
$avatar: user.avatar_url,
});
}
}, []);
const reset = useCallback(() => {
mixpanel.reset();
}, []);
const trackMixpanelEvent = useCallback(
(event: string, properties?: Dict) => {
try {
const currentUser = auth.getCurrentSession();
const props = {
...(properties || {}),
...(currentUser?.user_no ? { id: currentUser.user_no } : {}),
};
mixpanel.track(event, props);
} catch (e) {
logger.error('Error tracking mixpanel event', e);
}
},
[auth.getCurrentSession]
);
return {
setIdentity,
reset,
trackMixpanelEvent,
};
};

View File

@@ -0,0 +1,10 @@
import { useMediaQuery } from 'react-responsive';
export const useResponsive = () => {
// media queries from react-responsive package
const isMobile = useMediaQuery({ query: '(max-width: 576px)' });
const isTablet = useMediaQuery({ query: '(min-width: 576px)' });
const isDesktop = useMediaQuery({ query: '(min-width: 1024px)' });
return { isMobile, isTablet, isDesktop };
};

View File

@@ -0,0 +1,17 @@
import { useParams } from 'react-router-dom';
import { useAppSelector } from './useAppSelector';
// this custom hook return currently selected project
export const useSelectedProject = () => {
const { projectId } = useParams();
const projectList = useAppSelector(state => state.projectsReducer.projects);
const selectedProject = projectList.data.find(project => project.id === projectId);
try {
return selectedProject;
} catch (error) {
console.error('custom error: error in selecting a project');
}
};

View File

@@ -0,0 +1,16 @@
import { useEffect, useRef } from 'react';
import { SocketService } from '@services/socket/socket.service';
import { useSocket } from '@/socket/socketContext';
export const useSocketService = () => {
const { socket } = useSocket();
const socketService = useRef(SocketService.getInstance());
useEffect(() => {
if (socket) {
socketService.current.init(socket);
}
}, [socket]);
return socketService.current;
};

View File

@@ -0,0 +1,11 @@
import { useSearchParams } from 'react-router-dom';
const useTabSearchParam = () => {
const [searchParams] = useSearchParams();
const tab = searchParams.get('tab');
const projectView = tab === 'tasks-list' ? 'list' : 'kanban';
return { tab, projectView };
};
export default useTabSearchParam;

View File

@@ -0,0 +1,91 @@
import { useEffect, useCallback, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useAppSelector } from './useAppSelector';
import { useAppDispatch } from './useAppDispatch';
import { fetchTask, setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
/**
* A custom hook that synchronizes the task drawer state with the URL.
* When the task drawer is opened, it adds the task ID to the URL as a query parameter.
* When the task drawer is closed, it removes the task ID from the URL.
* It also checks for a task ID in the URL when the component mounts and opens the task drawer if one is found.
*/
const useTaskDrawerUrlSync = () => {
const [searchParams, setSearchParams] = useSearchParams();
const dispatch = useAppDispatch();
const { showTaskDrawer, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
const { projectId } = useAppSelector(state => state.projectReducer);
// Use a ref to track whether we're in the process of closing the drawer
const isClosingDrawer = useRef(false);
// Use a ref to track if we've already processed the initial URL
const initialUrlProcessed = useRef(false);
// Use a ref to track the last task ID we processed
const lastProcessedTaskId = useRef<string | null>(null);
// Function to clear the task parameter from URL
const clearTaskFromUrl = useCallback(() => {
if (searchParams.has('task')) {
// Set the flag to indicate we're closing the drawer
isClosingDrawer.current = true;
// Create a new URLSearchParams object to avoid modifying the current one
const newParams = new URLSearchParams(searchParams);
newParams.delete('task');
// Update the URL without triggering a navigation
setSearchParams(newParams, { replace: true });
// Reset the flag after a short delay
setTimeout(() => {
isClosingDrawer.current = false;
}, 200);
}
}, [searchParams, setSearchParams]);
// Check for task ID in URL when component mounts
useEffect(() => {
// Only process the URL once on initial mount
if (!initialUrlProcessed.current) {
const taskIdFromUrl = searchParams.get('task');
if (taskIdFromUrl && !showTaskDrawer && projectId && !isClosingDrawer.current) {
lastProcessedTaskId.current = taskIdFromUrl;
dispatch(setSelectedTaskId(taskIdFromUrl));
dispatch(setShowTaskDrawer(true));
// Fetch task data
dispatch(fetchTask({ taskId: taskIdFromUrl, projectId }));
}
initialUrlProcessed.current = true;
}
}, [searchParams, dispatch, showTaskDrawer, projectId]);
// Update URL when task drawer state changes
useEffect(() => {
// Don't update URL if we're in the process of closing
if (isClosingDrawer.current) return;
if (showTaskDrawer && selectedTaskId) {
// Don't update if it's the same task ID we already processed
if (lastProcessedTaskId.current === selectedTaskId) return;
// Add task ID to URL when drawer is opened
lastProcessedTaskId.current = selectedTaskId;
// Create a new URLSearchParams object to avoid modifying the current one
const newParams = new URLSearchParams(searchParams);
newParams.set('task', selectedTaskId);
// Update the URL without triggering a navigation
setSearchParams(newParams, { replace: true });
} else if (!showTaskDrawer && searchParams.has('task')) {
// Remove task ID from URL when drawer is closed
clearTaskFromUrl();
lastProcessedTaskId.current = null;
}
}, [showTaskDrawer, selectedTaskId, searchParams, setSearchParams, clearTaskFromUrl]);
return { clearTaskFromUrl };
};
export default useTaskDrawerUrlSync;

View File

@@ -0,0 +1,153 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { buildTimeString } from '@/utils/timeUtils';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import logger from '@/utils/errorLogger';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { updateTaskTimeTracking } from '@/features/tasks/tasks.slice';
import { useAppSelector } from '@/hooks/useAppSelector';
export const useTaskTimer = (taskId: string, initialStartTime: number | null) => {
const dispatch = useAppDispatch();
const { socket } = useSocket();
const DEFAULT_TIME_LEFT = buildTimeString(0, 0, 0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const hasInitialized = useRef(false); // Track if we've initialized
const activeTimers = useAppSelector(state => state.taskReducer.activeTimers);
const reduxStartTime = activeTimers[taskId];
const started = Boolean(reduxStartTime);
const [timeString, setTimeString] = useState(DEFAULT_TIME_LEFT);
const [localStarted, setLocalStarted] = useState(false);
const timerTick = useCallback(() => {
if (!reduxStartTime) return;
const now = Date.now();
const diff = Math.floor((now - reduxStartTime) / 1000);
const hours = Math.floor(diff / 3600);
const minutes = Math.floor((diff % 3600) / 60);
const seconds = diff % 60;
setTimeString(buildTimeString(hours, minutes, seconds));
}, [reduxStartTime]);
const clearTimerInterval = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [taskId]);
const resetTimer = useCallback(() => {
clearTimerInterval();
setTimeString(DEFAULT_TIME_LEFT);
setLocalStarted(false);
}, [clearTimerInterval, taskId]);
// Timer management effect
useEffect(() => {
if (started && localStarted && reduxStartTime) {
clearTimerInterval();
timerTick();
intervalRef.current = setInterval(timerTick, 1000);
} else {
clearTimerInterval();
setTimeString(DEFAULT_TIME_LEFT);
if (started !== localStarted) {
setLocalStarted(started);
}
}
return () => {
clearTimerInterval();
};
}, [reduxStartTime, started, localStarted, timerTick, clearTimerInterval, taskId]);
// Initialize timer only on first mount if Redux is unset
useEffect(() => {
if (!hasInitialized.current && initialStartTime && reduxStartTime === undefined) {
dispatch(updateTaskTimeTracking({ taskId, timeTracking: initialStartTime }));
setLocalStarted(true);
} else if (reduxStartTime && !localStarted) {
setLocalStarted(true);
}
hasInitialized.current = true; // Mark as initialized
}, [initialStartTime, reduxStartTime, taskId, dispatch]);
const handleStartTimer = useCallback(() => {
if (started || !taskId) return;
try {
const now = Date.now();
dispatch(updateTaskTimeTracking({ taskId, timeTracking: now }));
setLocalStarted(true);
socket?.emit(SocketEvents.TASK_TIMER_START.toString(), JSON.stringify({ task_id: taskId }));
} catch (error) {
logger.error('Error starting timer:', error);
}
}, [taskId, started, socket, dispatch]);
const handleStopTimer = useCallback(() => {
if (!taskId) return;
resetTimer();
socket?.emit(SocketEvents.TASK_TIMER_STOP.toString(), JSON.stringify({ task_id: taskId }));
dispatch(updateTaskTimeTracking({ taskId, timeTracking: null }));
}, [taskId, socket, dispatch, resetTimer]);
// Socket event listeners
useEffect(() => {
if (!socket) return;
const handleTimerStop = (data: string) => {
try {
const { task_id } = typeof data === 'string' ? JSON.parse(data) : data;
if (task_id === taskId) {
resetTimer();
dispatch(updateTaskTimeTracking({ taskId, timeTracking: null }));
}
} catch (error) {
logger.error('Error parsing timer stop event:', error);
}
};
const handleTimerStart = (data: string) => {
try {
const { task_id, start_time } = typeof data === 'string' ? JSON.parse(data) : data;
if (task_id === taskId && start_time) {
const time = typeof start_time === 'number' ? start_time : parseInt(start_time);
dispatch(updateTaskTimeTracking({ taskId, timeTracking: time }));
setLocalStarted(true);
}
} catch (error) {
logger.error('Error parsing timer start event:', error);
}
};
socket.on(SocketEvents.TASK_TIMER_STOP.toString(), handleTimerStop);
socket.on(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart);
return () => {
socket.off(SocketEvents.TASK_TIMER_STOP.toString(), handleTimerStop);
socket.off(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart);
};
}, [socket, taskId, dispatch, resetTimer]);
return {
started,
timeString,
handleStartTimer,
handleStopTimer,
};
};