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:
@@ -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
|
||||
|
||||
+27
-7
@@ -380,22 +380,23 @@
|
||||
|
||||
/* Larger Touch Targets for ADHD Mode */
|
||||
/* Only increase height, not width - width breaks flex layouts and icon buttons */
|
||||
.larger-targets button:not([data-size="icon"]),
|
||||
.larger-targets [role="button"]:not([data-size="icon"]) {
|
||||
/* Exclude switches, checkboxes, and sliders from larger targets - they have specific sizing */
|
||||
.larger-targets button:not([data-size="icon"]):not([role="switch"]):not([role="checkbox"]),
|
||||
.larger-targets [role="button"]:not([data-size="icon"]):not([role="switch"]):not([role="checkbox"]) {
|
||||
min-height: 44px;
|
||||
padding-top: 0.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
/* Icon buttons get increased size as square */
|
||||
.larger-targets button[data-size="icon"],
|
||||
.larger-targets [role="button"][data-size="icon"] {
|
||||
/* Icon buttons get increased size as square - but not switches */
|
||||
.larger-targets button[data-size="icon"]:not([role="switch"]),
|
||||
.larger-targets [role="button"][data-size="icon"]:not([role="switch"]) {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* Form inputs and selects */
|
||||
.larger-targets input,
|
||||
/* Form inputs and selects - exclude checkbox and switch */
|
||||
.larger-targets input:not([type="checkbox"]):not([role="switch"]),
|
||||
.larger-targets select,
|
||||
.larger-targets textarea {
|
||||
min-height: 44px;
|
||||
@@ -410,6 +411,25 @@
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
/* Checkboxes and switches should maintain their aspect ratio */
|
||||
.larger-targets [role="switch"],
|
||||
.larger-targets [role="checkbox"],
|
||||
.larger-targets input[type="checkbox"] {
|
||||
min-height: auto !important;
|
||||
min-width: auto !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Specific fix for Radix checkbox buttons - ensure square aspect ratio */
|
||||
.larger-targets button[role="checkbox"] {
|
||||
height: 1rem !important;
|
||||
width: 1rem !important;
|
||||
min-height: 1rem !important;
|
||||
min-width: 1rem !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Visual Timer Color Classes */
|
||||
.timer-green {
|
||||
color: hsl(var(--adhd-timer-green));
|
||||
|
||||
@@ -45,11 +45,16 @@ export default function ADHDDashboardPage() {
|
||||
return updated.toDateString() === today.toDateString();
|
||||
}).length;
|
||||
|
||||
const quickWins = tasks.filter(t =>
|
||||
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() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setLocation('/')}
|
||||
onClick={() => window.history.back()}
|
||||
className="shrink-0"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function BodyDoublingPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setLocation('/')}
|
||||
onClick={() => window.history.back()}
|
||||
className="shrink-0"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function QuickWinsPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setLocation('/')}
|
||||
onClick={() => window.history.back()}
|
||||
className="shrink-0"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function SingleTaskPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setLocation('/')}
|
||||
onClick={() => window.history.back()}
|
||||
className="shrink-0"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
|
||||
Reference in New Issue
Block a user