From 2a98ebdf9f2a2c1f47037e27566c9cdb0f976c4f Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Fri, 16 Jan 2026 23:01:26 +0100 Subject: [PATCH] fix: Multiple ADHD/Focus Tools fixes 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) --- .../adhd/providers/BreakReminderProvider.tsx | 102 ++++++++++++++---- client/src/index.css | 34 ++++-- client/src/pages/ADHDDashboardPage.tsx | 17 +-- client/src/pages/BodyDoublingPage.tsx | 2 +- client/src/pages/QuickWinsPage.tsx | 2 +- client/src/pages/SingleTaskPage.tsx | 2 +- 6 files changed, 121 insertions(+), 38 deletions(-) diff --git a/client/src/components/adhd/providers/BreakReminderProvider.tsx b/client/src/components/adhd/providers/BreakReminderProvider.tsx index 7d9ff21..afbc046 100644 --- a/client/src/components/adhd/providers/BreakReminderProvider.tsx +++ b/client/src/components/adhd/providers/BreakReminderProvider.tsx @@ -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(null); const [snoozeUntil, setSnoozeUntil] = useState(null); - // Load last break from localStorage on mount + // 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('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 ( - t.status !== 'done' && - t.estimatedDuration && - t.estimatedDuration <= 15 - ).length; + // Use same logic as QuickWinList for consistent count + const threshold = settings.quickWinThreshold || 15; + const quickWins = tasks.filter(t => { + if (t.status === 'done') return false; + // Only tasks without subtasks (leaf tasks) + if (tasks.some(parent => parent.parentTaskId === t.id)) return false; + // Filter by duration threshold (same as QuickWinList: threshold * 1.5) + if (t.estimatedDuration && t.estimatedDuration > threshold * 1.5) return false; + return true; + }).length; const features = [ { @@ -84,7 +89,7 @@ export default function ADHDDashboardPage() {