fix: Multiple ADHD/Focus Tools fixes
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)
This commit is contained in:
Paul Nothaft
2026-01-16 23:01:26 +01:00
parent b4e7cce651
commit 2a98ebdf9f
6 changed files with 121 additions and 38 deletions
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
import React, { createContext, useContext, useState, useEffect, useCallback, useRef, ReactNode } from 'react';
import { useADHDMode } from './ADHDModeProvider';
import { useMutation, useQueryClient } from '@tanstack/react-query';
@@ -22,17 +22,62 @@ export function BreakReminderProvider({ children }: { children: ReactNode }) {
const [lastBreakAt, setLastBreakAt] = useState<Date | null>(null);
const [snoozeUntil, setSnoozeUntil] = useState<Date | null>(null);
// Load last break from localStorage on mount
// 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('lastBreakAt');
if (stored) {
setLastBreakAt(new Date(stored));
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 {
// Default to current time on first load
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
@@ -54,9 +99,15 @@ export function BreakReminderProvider({ children }: { children: ReactNode }) {
setLastBreakAt(now);
localStorage.setItem('lastBreakAt', now.toISOString());
setIsReminderVisible(false);
setMinutesSinceBreak(0);
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]);
@@ -71,38 +122,45 @@ export function BreakReminderProvider({ children }: { children: ReactNode }) {
setIsReminderVisible(false);
}, []);
// Timer to track minutes since break and show reminder
// Timer to track ACTIVE minutes since break (only when page is visible)
useEffect(() => {
if (!isEnabled || !lastBreakAt) return;
if (!isEnabled) return;
const interval = setInterval(() => {
const now = new Date();
const diffMs = now.getTime() - lastBreakAt.getTime();
const diffMins = Math.floor(diffMs / 60000);
setMinutesSinceBreak(diffMins);
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 && now < snoozeUntil) {
if (snoozeUntil && new Date() < snoozeUntil) {
return;
}
if (diffMins >= reminderInterval && !isReminderVisible) {
if (accumulatedMinutesRef.current >= reminderInterval && !isReminderVisible) {
setIsReminderVisible(true);
setSnoozeUntil(null);
}
}, 30000); // Check every 30 seconds
// Initial check
const now = new Date();
const diffMs = now.getTime() - lastBreakAt.getTime();
const diffMins = Math.floor(diffMs / 60000);
setMinutesSinceBreak(diffMins);
return () => clearInterval(interval);
}, [isEnabled, lastBreakAt, settings.breakReminderInterval, snoozeUntil, isReminderVisible]);
}, [isEnabled, settings.breakReminderInterval, snoozeUntil, isReminderVisible]);
return (
<BreakReminderContext.Provider