import React, { createContext, useContext, useState, useEffect, useCallback, useRef, ReactNode } from 'react'; import { useADHDMode } from './ADHDModeProvider'; import { useMutation, useQueryClient } from '@tanstack/react-query'; interface BreakReminderContextType { isReminderVisible: boolean; minutesSinceBreak: number; logBreak: (breakType: string, durationMinutes?: number) => void; snooze: (minutes: number) => void; dismiss: () => void; lastBreakAt: Date | null; } const BreakReminderContext = createContext(undefined); export function BreakReminderProvider({ children }: { children: ReactNode }) { const { isEnabled, settings } = useADHDMode(); const queryClient = useQueryClient(); const [isReminderVisible, setIsReminderVisible] = useState(false); const [minutesSinceBreak, setMinutesSinceBreak] = useState(0); const [lastBreakAt, setLastBreakAt] = useState(null); const [snoozeUntil, setSnoozeUntil] = useState(null); // Track accumulated active minutes instead of time since break const accumulatedMinutesRef = useRef(0); const lastTickRef = useRef(Date.now()); const isPageVisibleRef = useRef(true); // Reset accumulated minutes when break is taken or on page load useEffect(() => { const stored = localStorage.getItem('accumulatedActiveMinutes'); const storedTime = localStorage.getItem('lastActiveTime'); if (stored && storedTime) { // Only restore if less than 8 hours have passed (assume they took a real break) const lastActive = new Date(storedTime).getTime(); const hoursSinceActive = (Date.now() - lastActive) / (1000 * 60 * 60); if (hoursSinceActive < 8) { accumulatedMinutesRef.current = parseInt(stored, 10) || 0; } else { // Reset after long break accumulatedMinutesRef.current = 0; localStorage.setItem('accumulatedActiveMinutes', '0'); } } // Legacy: load lastBreakAt for backward compatibility const storedBreak = localStorage.getItem('lastBreakAt'); if (storedBreak) { setLastBreakAt(new Date(storedBreak)); } else { const now = new Date(); setLastBreakAt(now); localStorage.setItem('lastBreakAt', now.toISOString()); } setMinutesSinceBreak(accumulatedMinutesRef.current); }, []); // Track page visibility useEffect(() => { const handleVisibilityChange = () => { const isVisible = document.visibilityState === 'visible'; if (!isVisible && isPageVisibleRef.current) { // Page becoming hidden - save current state localStorage.setItem('accumulatedActiveMinutes', String(accumulatedMinutesRef.current)); localStorage.setItem('lastActiveTime', new Date().toISOString()); } else if (isVisible && !isPageVisibleRef.current) { // Page becoming visible - update last tick to now to avoid counting hidden time lastTickRef.current = Date.now(); } isPageVisibleRef.current = isVisible; }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => document.removeEventListener('visibilitychange', handleVisibilityChange); }, []); // Log break mutation const logBreakMutation = useMutation({ mutationFn: async ({ breakType, durationMinutes }: { breakType: string; durationMinutes: number }) => { const res = await fetch('/api/user/log-break', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ breakType, durationMinutes }), }); if (!res.ok) throw new Error('Failed to log break'); return res.json(); }, }); const logBreak = useCallback((breakType: string, durationMinutes = 5) => { const now = new Date(); setLastBreakAt(now); localStorage.setItem('lastBreakAt', now.toISOString()); setIsReminderVisible(false); setSnoozeUntil(null); // Reset accumulated time accumulatedMinutesRef.current = 0; setMinutesSinceBreak(0); localStorage.setItem('accumulatedActiveMinutes', '0'); localStorage.setItem('lastActiveTime', now.toISOString()); lastTickRef.current = Date.now(); // Log to server logBreakMutation.mutate({ breakType, durationMinutes }); }, [logBreakMutation]); const snooze = useCallback((minutes: number) => { const snoozeTime = new Date(Date.now() + minutes * 60 * 1000); setSnoozeUntil(snoozeTime); setIsReminderVisible(false); }, []); const dismiss = useCallback(() => { setIsReminderVisible(false); }, []); // Timer to track ACTIVE minutes since break (only when page is visible) useEffect(() => { if (!isEnabled) return; const interval = setInterval(() => { const now = Date.now(); // Only count time if page is visible if (isPageVisibleRef.current) { const elapsedMs = now - lastTickRef.current; const elapsedMins = elapsedMs / 60000; // Add to accumulated time accumulatedMinutesRef.current += elapsedMins; setMinutesSinceBreak(Math.floor(accumulatedMinutesRef.current)); // Save periodically localStorage.setItem('accumulatedActiveMinutes', String(Math.floor(accumulatedMinutesRef.current))); localStorage.setItem('lastActiveTime', new Date().toISOString()); } lastTickRef.current = now; // Check if reminder should be shown const reminderInterval = settings.breakReminderInterval || 45; // Check snooze if (snoozeUntil && new Date() < snoozeUntil) { return; } if (accumulatedMinutesRef.current >= reminderInterval && !isReminderVisible) { setIsReminderVisible(true); setSnoozeUntil(null); } }, 30000); // Check every 30 seconds return () => clearInterval(interval); }, [isEnabled, settings.breakReminderInterval, snoozeUntil, isReminderVisible]); return ( {children} ); } export function useBreakReminder() { const context = useContext(BreakReminderContext); if (context === undefined) { throw new Error('useBreakReminder must be used within a BreakReminderProvider'); } return context; }