feat: Add Focus Tools (ADHD-friendly productivity features)
continuous-integration/drone/push Build is passing

- Add Focus Tools dashboard with collapsible help section
- Implement Quick Wins page for tasks under 15 minutes
- Add Single Task Focus mode to reduce overwhelm
- Create Body Doubling page for virtual co-working
- Add visual timer, break reminders, and energy tracking
- Implement hyperfocus protection alerts
- Add ADHD settings panel with customizable options
- Include full English and German translations
- Fix larger touch targets CSS to not break button layouts
- Add Playwright tests for Focus Tools features
This commit is contained in:
Paul Nothaft
2026-01-15 21:20:04 +01:00
parent 74ffef48d3
commit 7ce4f7efdc
31 changed files with 5651 additions and 11 deletions
@@ -0,0 +1,129 @@
import React, { createContext, useContext, useState, useEffect, useCallback, 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);
// Load last break from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem('lastBreakAt');
if (stored) {
setLastBreakAt(new Date(stored));
} else {
// Default to current time on first load
const now = new Date();
setLastBreakAt(now);
localStorage.setItem('lastBreakAt', now.toISOString());
}
}, []);
// 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);
setMinutesSinceBreak(0);
setSnoozeUntil(null);
// 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 minutes since break and show reminder
useEffect(() => {
if (!isEnabled || !lastBreakAt) return;
const interval = setInterval(() => {
const now = new Date();
const diffMs = now.getTime() - lastBreakAt.getTime();
const diffMins = Math.floor(diffMs / 60000);
setMinutesSinceBreak(diffMins);
// Check if reminder should be shown
const reminderInterval = settings.breakReminderInterval || 45;
// Check snooze
if (snoozeUntil && now < snoozeUntil) {
return;
}
if (diffMins >= 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]);
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;
}