init
This commit is contained in:
30
worklenz-frontend/src/hooks/useAlert.ts
Normal file
30
worklenz-frontend/src/hooks/useAlert.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
4
worklenz-frontend/src/hooks/useAppDispatch.ts
Normal file
4
worklenz-frontend/src/hooks/useAppDispatch.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { AppDispatch } from '../app/store';
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||
4
worklenz-frontend/src/hooks/useAppSelector.ts
Normal file
4
worklenz-frontend/src/hooks/useAppSelector.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../app/store';
|
||||
|
||||
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||
9
worklenz-frontend/src/hooks/useAuth.ts
Normal file
9
worklenz-frontend/src/hooks/useAuth.ts
Normal 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;
|
||||
};
|
||||
7
worklenz-frontend/src/hooks/useDoumentTItle.ts
Normal file
7
worklenz-frontend/src/hooks/useDoumentTItle.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useDocumentTitle = (title: string) => {
|
||||
return useEffect(() => {
|
||||
document.title = `Worklenz | ${title}`;
|
||||
}, [title]);
|
||||
};
|
||||
25
worklenz-frontend/src/hooks/useDragCursor.ts
Normal file
25
worklenz-frontend/src/hooks/useDragCursor.ts
Normal 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;
|
||||
16
worklenz-frontend/src/hooks/useIsProjectManager.ts
Normal file
16
worklenz-frontend/src/hooks/useIsProjectManager.ts
Normal 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;
|
||||
6
worklenz-frontend/src/hooks/useIsomorphicLayoutEffect.ts
Normal file
6
worklenz-frontend/src/hooks/useIsomorphicLayoutEffect.ts
Normal 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;
|
||||
63
worklenz-frontend/src/hooks/useMixpanelTracking.tsx
Normal file
63
worklenz-frontend/src/hooks/useMixpanelTracking.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
10
worklenz-frontend/src/hooks/useResponsive.ts
Normal file
10
worklenz-frontend/src/hooks/useResponsive.ts
Normal 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 };
|
||||
};
|
||||
17
worklenz-frontend/src/hooks/useSelectedProject.ts
Normal file
17
worklenz-frontend/src/hooks/useSelectedProject.ts
Normal 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');
|
||||
}
|
||||
};
|
||||
16
worklenz-frontend/src/hooks/useSocketService.ts
Normal file
16
worklenz-frontend/src/hooks/useSocketService.ts
Normal 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;
|
||||
};
|
||||
11
worklenz-frontend/src/hooks/useTabSearchParam.ts
Normal file
11
worklenz-frontend/src/hooks/useTabSearchParam.ts
Normal 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;
|
||||
91
worklenz-frontend/src/hooks/useTaskDrawerUrlSync.ts
Normal file
91
worklenz-frontend/src/hooks/useTaskDrawerUrlSync.ts
Normal 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;
|
||||
153
worklenz-frontend/src/hooks/useTaskTimer.ts
Normal file
153
worklenz-frontend/src/hooks/useTaskTimer.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user