import { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode, } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { timerApi } from "@/api/timer"; import type { OngoingTimer, TimeEntry } from "@/types"; interface TimerContextType { ongoingTimer: OngoingTimer | null; isLoading: boolean; elapsedSeconds: number; startTimer: (projectId?: string) => Promise; updateTimerProject: (projectId?: string | null) => Promise; updateTimerStartTime: (startTime: string) => Promise; stopTimer: (projectId?: string) => Promise; } const TimerContext = createContext(undefined); export function TimerProvider({ children }: { children: ReactNode }) { const queryClient = useQueryClient(); const [elapsedSeconds, setElapsedSeconds] = useState(0); // Use ref for interval ID to avoid stale closure issues const intervalRef = useRef | null>(null); const { data: ongoingTimer, isLoading } = useQuery({ queryKey: ["ongoingTimer"], queryFn: timerApi.getOngoing, refetchInterval: 60000, // Refetch every minute to sync with server }); // Calculate elapsed time useEffect(() => { if (ongoingTimer) { const startTime = new Date(ongoingTimer.startTime).getTime(); const now = Date.now(); const initialElapsed = Math.floor((now - startTime) / 1000); setElapsedSeconds(initialElapsed); // Clear any existing interval first if (intervalRef.current) { clearInterval(intervalRef.current); } // Start interval to update elapsed time intervalRef.current = setInterval(() => { setElapsedSeconds(Math.floor((Date.now() - startTime) / 1000)); }, 1000); } else { setElapsedSeconds(0); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } } // Cleanup on unmount or when ongoingTimer changes return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [ongoingTimer]); // Start timer mutation const startMutation = useMutation({ mutationFn: timerApi.start, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["ongoingTimer"] }); }, }); // Update timer mutation const updateMutation = useMutation({ mutationFn: timerApi.update, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["ongoingTimer"] }); }, }); // Stop timer mutation const stopMutation = useMutation({ mutationFn: timerApi.stop, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["ongoingTimer"] }); queryClient.invalidateQueries({ queryKey: ["timeEntries"] }); }, }); const startTimer = useCallback( async (projectId?: string) => { await startMutation.mutateAsync(projectId); }, [startMutation], ); const updateTimerProject = useCallback( async (projectId?: string | null) => { await updateMutation.mutateAsync({ projectId }); }, [updateMutation], ); const updateTimerStartTime = useCallback( async (startTime: string) => { await updateMutation.mutateAsync({ startTime }); }, [updateMutation], ); const stopTimer = useCallback( async (projectId?: string): Promise => { try { const entry = await stopMutation.mutateAsync(projectId); return entry; } catch { return null; } }, [stopMutation], ); return ( {children} ); } export function useTimer() { const context = useContext(TimerContext); if (context === undefined) { throw new Error("useTimer must be used within a TimerProvider"); } return context; }