2a98ebdf9f
continuous-integration/drone/push Build is passing
Switch/Checkbox Styling:
- Exclude switches and checkboxes from larger-targets CSS
- Fix checkbox square aspect ratio when ADHD mode enabled
- Prevent padding from being applied to form controls
Quick Wins Count:
- Use settings.quickWinThreshold instead of hardcoded 15
- Match same filtering logic as QuickWinList component
Back Button Navigation:
- Use window.history.back() instead of setLocation('/')
- Works correctly when navigating from ADHD dashboard to sub-pages
Time Tracking (Break Reminder):
- Track only active browser time using Page Visibility API
- Reset accumulated time after 8 hours of inactivity
- Store/restore active time in localStorage properly
- No more 1500+ hour counts from leaving browser open
Sources:
- https://ui.shadcn.com/docs/theming (shadcn theming)
- https://ui.shadcn.com/docs/tailwind-v4 (Tailwind v4 updates)
188 lines
6.3 KiB
TypeScript
188 lines
6.3 KiB
TypeScript
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<BreakReminderContextType | undefined>(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<Date | null>(null);
|
|
const [snoozeUntil, setSnoozeUntil] = useState<Date | null>(null);
|
|
|
|
// Track accumulated active minutes instead of time since break
|
|
const accumulatedMinutesRef = useRef(0);
|
|
const lastTickRef = useRef<number>(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 (
|
|
<BreakReminderContext.Provider
|
|
value={{
|
|
isReminderVisible,
|
|
minutesSinceBreak,
|
|
logBreak,
|
|
snooze,
|
|
dismiss,
|
|
lastBreakAt,
|
|
}}
|
|
>
|
|
{children}
|
|
</BreakReminderContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useBreakReminder() {
|
|
const context = useContext(BreakReminderContext);
|
|
if (context === undefined) {
|
|
throw new Error('useBreakReminder must be used within a BreakReminderProvider');
|
|
}
|
|
return context;
|
|
}
|