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
+47 -2
View File
@@ -16,6 +16,17 @@ import {
import { Switch, Route, useLocation } from "wouter";
// ADHD Mode Components
import {
ADHDModeProvider,
useADHDMode,
BreakReminderProvider,
BreakReminderOverlay,
EnergyCheckIn,
HyperfocusGuard,
EncouragementToast
} from '@/components/adhd';
// Components
import TaskCreationModal from './components/TaskCreationModal';
import TaskDetailsModal from './components/TaskDetailsModal';
@@ -61,11 +72,19 @@ import { Loader2 } from "lucide-react";
import TimeTrackingPage from './pages/TimeTrackingPage';
function App() {
// ADHD Pages
import QuickWinsPage from './pages/QuickWinsPage';
import SingleTaskPage from './pages/SingleTaskPage';
import BodyDoublingPage from './pages/BodyDoublingPage';
import ADHDDashboardPage from './pages/ADHDDashboardPage';
// Inner App component that uses ADHD hooks
function AppContent() {
const { t } = useTranslation();
const { toast } = useToast();
const [, setLocation] = useLocation();
const isBlocked = useRoutineBlocker();
const { isEnabled: adhdEnabled, settings: adhdSettings } = useADHDMode();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false);
@@ -317,7 +336,7 @@ function App() {
<SidebarProvider>
<AppSidebar user={user} />
<SidebarInset>
<div className="min-h-screen bg-background flex flex-col">
<div className={`min-h-screen bg-background flex flex-col ${adhdEnabled ? 'adhd-mode' : ''} ${adhdEnabled && adhdSettings.reducedAnimations ? 'reduced-motion' : ''} ${adhdEnabled && adhdSettings.largerTargets ? 'larger-targets' : ''}`}>
<main className="flex-1 p-4 md:p-6 max-w-screen-2xl mx-auto w-full relative">
<Switch>
<Route path="/">
@@ -407,6 +426,9 @@ function App() {
<Route path="/notifications">
<NotificationsPage />
</Route>
<Route path="/time-tracking">
<TimeTrackingPage />
</Route>
<Route path="/settings">
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
</Route>
@@ -420,6 +442,13 @@ function App() {
)}
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
{/* ADHD Mode Routes */}
<Route path="/adhd" component={ADHDDashboardPage} />
<Route path="/adhd/quick-wins" component={QuickWinsPage} />
<Route path="/adhd/single-task" component={SingleTaskPage} />
<Route path="/adhd/body-doubling" component={BodyDoublingPage} />
<Route component={NotFound} />
</Switch>
</main>
@@ -450,6 +479,11 @@ function App() {
<Toaster />
{/* ADHD Mode Overlays */}
{adhdEnabled && <BreakReminderOverlay />}
{adhdEnabled && adhdSettings.hyperfocusProtection && <HyperfocusGuard maxMinutes={adhdSettings.hyperfocusMaxMinutes} />}
{adhdEnabled && adhdSettings.positiveMessagingLevel !== 'off' && <EncouragementToast />}
<TaskCreationModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
@@ -499,5 +533,16 @@ function App() {
}
// Main App wrapper with ADHD providers
function App() {
return (
<ADHDModeProvider>
<BreakReminderProvider>
<AppContent />
</BreakReminderProvider>
</ADHDModeProvider>
);
}
export default App;
+2 -1
View File
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Home, Calendar, List, LayoutGrid, Settings, CheckSquare, Target, Trophy, LogOut, Award, Bot, CalendarOff, Bell, Clock } from 'lucide-react';
import { Home, Calendar, List, LayoutGrid, Settings, CheckSquare, Target, Trophy, LogOut, Award, Bot, CalendarOff, Bell, Clock, Brain } from 'lucide-react';
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query';
import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
@@ -58,6 +58,7 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) {
{ title: t('navigation.weekList'), id: 'weeklist', path: '/weeklist', icon: List, color: 'text-pink-500' },
{ title: t('unscheduled.title', 'Unscheduled'), id: 'unscheduled', path: '/unscheduled', icon: CalendarOff, color: 'text-slate-500' },
{ title: t('navigation.kanban'), id: 'kanban', path: '/kanban', icon: LayoutGrid, color: 'text-orange-500' },
{ title: t('navigation.adhd', 'ADHD Mode'), id: 'adhd', path: '/adhd', icon: Brain, color: 'text-purple-500' },
{ title: t('navigation.achievements'), id: 'achievements', path: '/achievements', icon: Trophy, color: 'text-yellow-500' },
{ title: t('navigation.leaderboard'), id: 'leaderboard', path: '/leaderboard', icon: Award, color: 'text-yellow-500' },
{ title: t('analytics.timeTracking'), id: 'time-tracking', path: '/time-tracking', icon: Clock, color: 'text-teal-500' },
@@ -0,0 +1,88 @@
import { useADHDMode } from './providers/ADHDModeProvider';
import { Switch } from '@/components/ui/switch';
import { Brain, Sparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
interface ADHDModeToggleProps {
showLabel?: boolean;
compact?: boolean;
}
export function ADHDModeToggle({ showLabel = true, compact = false }: ADHDModeToggleProps) {
const { isEnabled, toggle, isLoading } = useADHDMode();
const { t } = useTranslation();
if (compact) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={toggle}
disabled={isLoading}
className={`
relative flex items-center justify-center w-10 h-10 rounded-lg
transition-all duration-200
${isEnabled
? 'bg-primary/20 text-primary border-2 border-primary'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}
${isLoading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
>
<Brain className="h-5 w-5" />
{isEnabled && (
<Sparkles className="absolute -top-1 -right-1 h-3 w-3 text-primary animate-pulse" />
)}
</button>
</TooltipTrigger>
<TooltipContent>
<p>{isEnabled ? t('adhd.modeEnabled') : t('adhd.modeDisabled')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return (
<div className="flex items-center gap-3 p-3 rounded-lg bg-card border">
<div className={`
flex items-center justify-center w-10 h-10 rounded-full
${isEnabled ? 'bg-primary/20' : 'bg-muted'}
`}>
<Brain className={`h-5 w-5 ${isEnabled ? 'text-primary' : 'text-muted-foreground'}`} />
</div>
<div className="flex-1">
{showLabel && (
<>
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{t('adhd.mode')}</span>
{isEnabled && (
<span className="text-xs px-2 py-0.5 rounded-full bg-primary/20 text-primary">
{t('adhd.active')}
</span>
)}
</div>
<p className="text-xs text-muted-foreground">
{t('adhd.modeDescription')}
</p>
</>
)}
</div>
<Switch
checked={isEnabled}
onCheckedChange={toggle}
disabled={isLoading}
aria-label={t('adhd.toggleMode')}
/>
</div>
);
}
@@ -0,0 +1,256 @@
import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { motion, AnimatePresence } from 'framer-motion';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Loader2, Sparkles, Clock, CheckCircle2, AlertCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useADHDMode } from '../providers/ADHDModeProvider';
import type { Task } from '@shared/schema';
interface SubtaskSuggestion {
title: string;
estimatedMinutes: number;
order: number;
}
interface BreakdownResponse {
subtasks: SubtaskSuggestion[];
totalEstimatedMinutes: number;
encouragement: string;
}
interface TaskBreakdownModalProps {
task: Task;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function TaskBreakdownModal({ task, open, onOpenChange }: TaskBreakdownModalProps) {
const { t } = useTranslation();
const { settings } = useADHDMode();
const queryClient = useQueryClient();
const [breakdown, setBreakdown] = useState<BreakdownResponse | null>(null);
const [selectedSubtasks, setSelectedSubtasks] = useState<Set<number>>(new Set());
// Mutation to get AI breakdown
const breakdownMutation = useMutation({
mutationFn: async () => {
const res = await fetch('/api/ai/breakdown-task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
taskId: task.id,
title: task.title,
description: task.description,
estimatedDuration: task.estimatedDuration,
}),
});
if (!res.ok) throw new Error('Failed to break down task');
return res.json() as Promise<BreakdownResponse>;
},
onSuccess: (data) => {
setBreakdown(data);
// Select all by default
setSelectedSubtasks(new Set(data.subtasks.map((_, i) => i)));
},
});
// Mutation to create subtasks
const createSubtasksMutation = useMutation({
mutationFn: async (subtasks: SubtaskSuggestion[]) => {
const promises = subtasks.map((subtask, index) =>
fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title: subtask.title,
parentTaskId: task.id,
estimatedDuration: subtask.estimatedMinutes,
priority: task.priority,
status: 'todo',
}),
})
);
return Promise.all(promises);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
onOpenChange(false);
},
});
const handleGenerate = () => {
breakdownMutation.mutate();
};
const handleToggleSubtask = (index: number) => {
const newSet = new Set(selectedSubtasks);
if (newSet.has(index)) {
newSet.delete(index);
} else {
newSet.add(index);
}
setSelectedSubtasks(newSet);
};
const handleCreate = () => {
if (!breakdown) return;
const selected = breakdown.subtasks.filter((_, i) => selectedSubtasks.has(i));
createSubtasksMutation.mutate(selected);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
{t('adhd.taskBreakdown.title')}
</DialogTitle>
<DialogDescription>
{t('adhd.taskBreakdown.description')}
</DialogDescription>
</DialogHeader>
{/* Task info */}
<div className="bg-muted/50 rounded-lg p-4 mb-4">
<h4 className="font-medium mb-1">{task.title}</h4>
{task.description && (
<p className="text-sm text-muted-foreground">{task.description}</p>
)}
{task.estimatedDuration && (
<div className="flex items-center gap-1 mt-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
<span>{task.estimatedDuration} min {t('adhd.taskBreakdown.estimated')}</span>
</div>
)}
</div>
{/* Generate button or results */}
{!breakdown ? (
<div className="flex flex-col items-center py-8">
<p className="text-sm text-muted-foreground mb-4 text-center">
{t('adhd.taskBreakdown.prompt')}
</p>
<Button
onClick={handleGenerate}
disabled={breakdownMutation.isPending}
size="lg"
className={settings.largerTargets ? 'h-14 px-8 text-lg' : ''}
>
{breakdownMutation.isPending ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
{t('adhd.taskBreakdown.analyzing')}
</>
) : (
<>
<Sparkles className="h-5 w-5 mr-2" />
{t('adhd.taskBreakdown.breakItDown')}
</>
)}
</Button>
</div>
) : (
<div className="space-y-4">
{/* Encouragement */}
{breakdown.encouragement && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-primary/10 text-primary rounded-lg p-3 text-sm"
>
{breakdown.encouragement}
</motion.div>
)}
{/* Subtask list */}
<div className="space-y-2 max-h-[300px] overflow-y-auto">
<AnimatePresence>
{breakdown.subtasks.map((subtask, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className={`
flex items-start gap-3 p-3 rounded-lg border
${selectedSubtasks.has(index) ? 'bg-primary/5 border-primary/30' : 'bg-muted/30 border-transparent'}
transition-colors duration-200
`}
>
<Checkbox
checked={selectedSubtasks.has(index)}
onCheckedChange={() => handleToggleSubtask(index)}
className="mt-0.5"
/>
<div className="flex-1">
<p className="font-medium text-sm">{subtask.title}</p>
<div className="flex items-center gap-1 mt-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<span>~{subtask.estimatedMinutes} min</span>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
{/* Total time */}
<div className="flex items-center justify-between text-sm bg-muted/50 rounded-lg p-3">
<span className="text-muted-foreground">{t('adhd.taskBreakdown.totalTime')}</span>
<span className="font-medium">
{breakdown.subtasks
.filter((_, i) => selectedSubtasks.has(i))
.reduce((sum, s) => sum + s.estimatedMinutes, 0)} min
</span>
</div>
{/* Actions */}
<div className="flex gap-3 pt-2">
<Button
variant="outline"
onClick={handleGenerate}
disabled={breakdownMutation.isPending}
className="flex-1"
>
{t('adhd.taskBreakdown.regenerate')}
</Button>
<Button
onClick={handleCreate}
disabled={createSubtasksMutation.isPending || selectedSubtasks.size === 0}
className="flex-1"
>
{createSubtasksMutation.isPending ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<CheckCircle2 className="h-4 w-4 mr-2" />
)}
{t('adhd.taskBreakdown.createSubtasks', { count: selectedSubtasks.size })}
</Button>
</div>
</div>
)}
{/* Error state */}
{breakdownMutation.isError && (
<div className="flex items-center gap-2 text-destructive text-sm mt-2">
<AlertCircle className="h-4 w-4" />
<span>{t('adhd.taskBreakdown.error')}</span>
</div>
)}
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,124 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useBreakReminder } from '../providers/BreakReminderProvider';
import { useADHDMode } from '../providers/ADHDModeProvider';
import { Button } from '@/components/ui/button';
import { X, Clock, Droplet, PersonStanding, Eye, Apple, Wind, Timer } from 'lucide-react';
import { useTranslation } from 'react-i18next';
const breakSuggestions = [
{ id: 'stretch', icon: PersonStanding, duration: 2 },
{ id: 'water', icon: Droplet, duration: 1 },
{ id: 'walk', icon: Timer, duration: 5 },
{ id: 'eyes', icon: Eye, duration: 1 },
{ id: 'snack', icon: Apple, duration: 3 },
{ id: 'breathe', icon: Wind, duration: 2 },
];
export function BreakReminderOverlay() {
const { t } = useTranslation();
const { isReminderVisible, minutesSinceBreak, logBreak, snooze, dismiss } = useBreakReminder();
const { isEnabled, settings } = useADHDMode();
if (!isEnabled || !isReminderVisible) return null;
return (
<AnimatePresence>
<motion.div
initial={{ x: '100%', opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: '100%', opacity: 0 }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className={`
fixed right-4 top-1/2 -translate-y-1/2 z-50
w-80 max-w-[calc(100vw-2rem)]
bg-gradient-to-br from-blue-50 to-purple-50
dark:from-blue-950/90 dark:to-purple-950/90
rounded-2xl shadow-2xl border border-border/50
backdrop-blur-sm
${settings.reducedAnimations ? '' : 'animate-pulse-soft'}
`}
>
{/* Dismiss button */}
<button
onClick={dismiss}
className="absolute top-3 right-3 p-1 rounded-full hover:bg-white/50 dark:hover:bg-black/20 transition-colors"
>
<X className="h-4 w-4 text-muted-foreground" />
</button>
<div className="p-6">
{/* Header */}
<div className="flex items-center gap-3 mb-4">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-primary/20">
<Clock className="h-6 w-6 text-primary" />
</div>
<div>
<h3 className="font-semibold text-lg">{t('adhd.breakReminder.title')}</h3>
<p className="text-sm text-muted-foreground">
{t('adhd.breakReminder.workingFor', { minutes: minutesSinceBreak })}
</p>
</div>
</div>
{/* Message */}
<p className="text-sm text-foreground/80 mb-4">
{t('adhd.breakReminder.message')}
</p>
{/* Break suggestions */}
<div className="grid grid-cols-3 gap-2 mb-4">
{breakSuggestions.map((suggestion) => {
const Icon = suggestion.icon;
return (
<button
key={suggestion.id}
onClick={() => logBreak(suggestion.id, suggestion.duration)}
className={`
flex flex-col items-center gap-1 p-3 rounded-lg
bg-white/50 dark:bg-black/20 hover:bg-white/80 dark:hover:bg-black/40
transition-all duration-200
${settings.largerTargets ? 'min-h-[80px]' : 'min-h-[60px]'}
`}
>
<Icon className="h-5 w-5 text-primary" />
<span className="text-xs font-medium">
{t(`adhd.breakReminder.types.${suggestion.id}`)}
</span>
<span className="text-[10px] text-muted-foreground">
{suggestion.duration} min
</span>
</button>
);
})}
</div>
{/* Snooze options */}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => snooze(5)}
className="flex-1 text-xs"
>
{t('adhd.breakReminder.snooze5')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => snooze(15)}
className="flex-1 text-xs"
>
{t('adhd.breakReminder.snooze15')}
</Button>
</div>
{/* Gentle message */}
<p className="text-xs text-center text-muted-foreground mt-4 italic">
{t('adhd.breakReminder.gentle')}
</p>
</div>
</motion.div>
</AnimatePresence>
);
}
@@ -0,0 +1,150 @@
import React, { useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useADHDMode } from '../providers/ADHDModeProvider';
import { Sparkles, Heart, Star, Trophy, Sun } from 'lucide-react';
import { useTranslation } from 'react-i18next';
type EncouragementType = 'taskComplete' | 'returning' | 'struggling' | 'streakBroken' | 'milestone' | 'quickWin';
interface EncouragementToastProps {
type: EncouragementType;
visible: boolean;
onDismiss: () => void;
customMessage?: string;
xpEarned?: number;
}
const icons: Record<EncouragementType, React.ComponentType<{ className?: string }>> = {
taskComplete: Star,
returning: Sun,
struggling: Heart,
streakBroken: Heart,
milestone: Trophy,
quickWin: Sparkles,
};
const colors: Record<EncouragementType, string> = {
taskComplete: 'from-green-500/20 to-emerald-500/20 border-green-500/30',
returning: 'from-blue-500/20 to-cyan-500/20 border-blue-500/30',
struggling: 'from-purple-500/20 to-pink-500/20 border-purple-500/30',
streakBroken: 'from-orange-500/20 to-amber-500/20 border-orange-500/30',
milestone: 'from-yellow-500/20 to-amber-500/20 border-yellow-500/30',
quickWin: 'from-primary/20 to-primary/10 border-primary/30',
};
export function EncouragementToast({
type,
visible,
onDismiss,
customMessage,
xpEarned,
}: EncouragementToastProps) {
const { t } = useTranslation();
const { isEnabled, settings } = useADHDMode();
const Icon = icons[type];
const colorClass = colors[type];
// Auto-dismiss after 4 seconds
useEffect(() => {
if (visible) {
const timer = setTimeout(onDismiss, 4000);
return () => clearTimeout(timer);
}
}, [visible, onDismiss]);
// Don't show if ADHD mode is disabled or messaging is minimal
if (!isEnabled || settings.positiveMessagingLevel === 'minimal') {
return null;
}
const messages = t(`adhd.encouragements.${type}`, { returnObjects: true }) as string[];
const message = customMessage || (Array.isArray(messages) ? messages[Math.floor(Math.random() * messages.length)] : messages);
return (
<AnimatePresence>
{visible && (
<motion.div
initial={{ opacity: 0, y: 50, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }}
transition={{
type: 'spring',
damping: 20,
stiffness: 300,
duration: settings.reducedAnimations ? 0 : undefined,
}}
onClick={onDismiss}
className={`
fixed bottom-6 left-1/2 -translate-x-1/2 z-50
bg-gradient-to-r ${colorClass}
backdrop-blur-md rounded-2xl border shadow-lg
p-4 cursor-pointer
max-w-md w-[calc(100vw-2rem)]
`}
>
<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-background/50">
<Icon className="h-6 w-6 text-primary" />
</div>
<div className="flex-1">
<p className="font-medium text-foreground">{message}</p>
{xpEarned && xpEarned > 0 && (
<p className="text-sm text-muted-foreground mt-1">
+{xpEarned} XP
</p>
)}
</div>
</div>
{/* Decorative sparkles for high messaging level */}
{settings.positiveMessagingLevel === 'high' && !settings.reducedAnimations && (
<>
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
className="absolute -top-2 -right-2"
>
<Sparkles className="h-5 w-5 text-yellow-500" />
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3 }}
className="absolute top-1/2 -left-2"
>
<Star className="h-4 w-4 text-primary/50" />
</motion.div>
</>
)}
</motion.div>
)}
</AnimatePresence>
);
}
// Hook to manage encouragement state
export function useEncouragement() {
const [visible, setVisible] = React.useState(false);
const [type, setType] = React.useState<EncouragementType>('taskComplete');
const [message, setMessage] = React.useState<string | undefined>();
const [xp, setXp] = React.useState<number | undefined>();
const show = React.useCallback((
encouragementType: EncouragementType,
customMessage?: string,
xpEarned?: number
) => {
setType(encouragementType);
setMessage(customMessage);
setXp(xpEarned);
setVisible(true);
}, []);
const hide = React.useCallback(() => {
setVisible(false);
}, []);
return { visible, type, message, xp, show, hide };
}
@@ -0,0 +1,277 @@
import React, { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Play, Pause, CheckCircle2, ArrowRight, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTranslation } from 'react-i18next';
import { useADHDMode } from '../providers/ADHDModeProvider';
import type { Task } from '@shared/schema';
interface FiveMinuteStarterProps {
task: Task;
onClose: () => void;
onComplete: () => void;
onContinue?: () => void;
}
type Phase = 'ready' | 'running' | 'done';
export function FiveMinuteStarter({
task,
onClose,
onComplete,
onContinue,
}: FiveMinuteStarterProps) {
const { t } = useTranslation();
const { settings } = useADHDMode();
const [phase, setPhase] = useState<Phase>('ready');
const [timeLeft, setTimeLeft] = useState(5 * 60); // 5 minutes in seconds
const [isRunning, setIsRunning] = useState(false);
const progress = ((5 * 60 - timeLeft) / (5 * 60)) * 100;
// Timer logic
useEffect(() => {
if (!isRunning || timeLeft <= 0) return;
const interval = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
setIsRunning(false);
setPhase('done');
// Play completion sound
const audio = new Audio('/sounds/complete.mp3');
audio.volume = 0.3;
audio.play().catch(() => {});
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(interval);
}, [isRunning, timeLeft]);
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const handleStart = () => {
setPhase('running');
setIsRunning(true);
};
const handlePause = () => {
setIsRunning(!isRunning);
};
const handleDone = () => {
onComplete();
onClose();
};
const handleContinue = () => {
// Reset for another 5 minutes
setTimeLeft(5 * 60);
setPhase('running');
setIsRunning(true);
onContinue?.();
};
const handleTakeBreak = () => {
// Just close without completing
onClose();
};
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm"
>
<div className="relative w-full max-w-md bg-card rounded-2xl border shadow-2xl p-6">
{/* Close button */}
<button
onClick={onClose}
className="absolute top-4 right-4 p-2 rounded-full hover:bg-muted transition-colors"
>
<X className="h-5 w-5 text-muted-foreground" />
</button>
<AnimatePresence mode="wait">
{phase === 'ready' && (
<motion.div
key="ready"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="text-center"
>
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-primary/10 flex items-center justify-center">
<Play className="h-10 w-10 text-primary" />
</div>
<h3 className="text-xl font-bold mb-2">
{t('adhd.fiveMin.title')}
</h3>
<p className="text-muted-foreground mb-2">
{task.title}
</p>
<p className="text-sm text-muted-foreground mb-6">
{t('adhd.fiveMin.prompt')}
</p>
<Button
size="lg"
onClick={handleStart}
className={`w-full ${settings.largerTargets ? 'h-14 text-lg' : 'h-12'}`}
>
<Play className="h-5 w-5 mr-2" />
{t('adhd.fiveMin.start')}
</Button>
<p className="text-xs text-muted-foreground mt-4 italic">
{t('adhd.fiveMin.noObligation')}
</p>
</motion.div>
)}
{phase === 'running' && (
<motion.div
key="running"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="text-center"
>
{/* Visual progress ring */}
<div className="relative w-48 h-48 mx-auto mb-6">
<svg className="w-full h-full transform -rotate-90">
<circle
cx="96"
cy="96"
r="88"
stroke="currentColor"
strokeWidth="8"
fill="none"
className="text-muted"
/>
<motion.circle
cx="96"
cy="96"
r="88"
stroke="currentColor"
strokeWidth="8"
fill="none"
className="text-primary"
strokeLinecap="round"
strokeDasharray={553}
animate={{ strokeDashoffset: 553 - (553 * progress) / 100 }}
transition={{ duration: 0.5 }}
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-4xl font-mono font-bold">
{formatTime(timeLeft)}
</span>
<span className="text-sm text-muted-foreground">
{t('adhd.fiveMin.remaining')}
</span>
</div>
</div>
<p className="font-medium mb-4 truncate px-4">
{task.title}
</p>
<div className="flex gap-3">
<Button
variant="outline"
onClick={handlePause}
className="flex-1"
>
{isRunning ? (
<>
<Pause className="h-4 w-4 mr-2" />
{t('adhd.fiveMin.pause')}
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
{t('adhd.fiveMin.resume')}
</>
)}
</Button>
<Button onClick={handleDone} className="flex-1">
<CheckCircle2 className="h-4 w-4 mr-2" />
{t('adhd.fiveMin.done')}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-4">
{t('adhd.fiveMin.encouragement')}
</p>
</motion.div>
)}
{phase === 'done' && (
<motion.div
key="done"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
className="text-center"
>
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-green-500/20 flex items-center justify-center">
<CheckCircle2 className="h-10 w-10 text-green-500" />
</div>
<h3 className="text-xl font-bold mb-2">
{t('adhd.fiveMin.congrats')}
</h3>
<p className="text-muted-foreground mb-6">
{t('adhd.fiveMin.fiveMinDone')}
</p>
<div className="flex flex-col gap-3">
<Button
size="lg"
onClick={handleContinue}
className={`w-full ${settings.largerTargets ? 'h-14' : 'h-12'}`}
>
<ArrowRight className="h-5 w-5 mr-2" />
{t('adhd.fiveMin.continueWorking')}
</Button>
<div className="flex gap-3">
<Button
variant="outline"
onClick={handleDone}
className="flex-1"
>
<CheckCircle2 className="h-4 w-4 mr-2" />
{t('adhd.fiveMin.finished')}
</Button>
<Button
variant="ghost"
onClick={handleTakeBreak}
className="flex-1"
>
{t('adhd.fiveMin.takeBreak')}
</Button>
</div>
</div>
<p className="text-sm text-primary mt-4 font-medium">
+10 XP {t('adhd.fiveMin.earned')}
</p>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
);
}
@@ -0,0 +1,184 @@
import React, { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useQuery } from '@tanstack/react-query';
import { AlertTriangle, Clock, ListTodo, Coffee, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTranslation } from 'react-i18next';
import { useADHDMode } from '../providers/ADHDModeProvider';
import type { Task } from '@shared/schema';
interface HyperfocusGuardProps {
activeTaskId?: string;
onTaskStart?: (taskId: string) => void;
}
export function HyperfocusGuard({ activeTaskId, onTaskStart }: HyperfocusGuardProps) {
const { t } = useTranslation();
const { isEnabled, settings } = useADHDMode();
const [focusStartTime, setFocusStartTime] = useState<Date | null>(null);
const [minutesOnTask, setMinutesOnTask] = useState(0);
const [showWarning, setShowWarning] = useState(false);
const [dismissed, setDismissed] = useState(false);
const { data: tasks = [] } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
// Track when focus starts
useEffect(() => {
if (activeTaskId && !focusStartTime) {
setFocusStartTime(new Date());
setDismissed(false);
} else if (!activeTaskId) {
setFocusStartTime(null);
setMinutesOnTask(0);
setShowWarning(false);
setDismissed(false);
}
}, [activeTaskId, focusStartTime]);
// Timer to track focus duration
useEffect(() => {
if (!isEnabled || !settings.hyperfocusProtection || !focusStartTime || dismissed) {
return;
}
const interval = setInterval(() => {
const now = new Date();
const diffMs = now.getTime() - focusStartTime.getTime();
const diffMins = Math.floor(diffMs / 60000);
setMinutesOnTask(diffMins);
// Show warning at threshold
if (diffMins >= settings.hyperfocusMaxMinutes && !showWarning) {
setShowWarning(true);
}
}, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, [isEnabled, settings, focusStartTime, showWarning, dismissed]);
// Count pending tasks
const pendingTasks = tasks.filter(
(t) => t.status !== 'done' && t.id !== activeTaskId
);
const urgentTasks = pendingTasks.filter(
(t) => t.priority === 'high' || (t.dueDate && new Date(t.dueDate) < new Date(Date.now() + 24 * 60 * 60 * 1000))
);
const handleDismiss = () => {
setShowWarning(false);
setDismissed(true);
};
const handleTakeBreak = () => {
setShowWarning(false);
// Could trigger break reminder or navigate to break
};
const handleSwitchTask = (taskId: string) => {
setShowWarning(false);
setFocusStartTime(null);
onTaskStart?.(taskId);
};
if (!isEnabled || !settings.hyperfocusProtection || !showWarning) {
return null;
}
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 50 }}
className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 w-full max-w-md px-4"
>
<div className="bg-amber-50 dark:bg-amber-950/90 border-2 border-amber-500/50 rounded-2xl shadow-xl p-5">
{/* Header */}
<div className="flex items-start gap-3 mb-4">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-amber-500/20">
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-400" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-amber-900 dark:text-amber-100">
{t('adhd.hyperfocus.title')}
</h4>
<p className="text-sm text-amber-700 dark:text-amber-300">
{t('adhd.hyperfocus.message', { minutes: minutesOnTask })}
</p>
</div>
<button
onClick={handleDismiss}
className="p-1 rounded-full hover:bg-amber-500/20 transition-colors"
>
<X className="h-4 w-4 text-amber-600 dark:text-amber-400" />
</button>
</div>
{/* Stats */}
<div className="flex gap-4 mb-4 text-sm">
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-300">
<Clock className="h-4 w-4" />
<span>{minutesOnTask} {t('adhd.hyperfocus.minutes')}</span>
</div>
{urgentTasks.length > 0 && (
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-300">
<ListTodo className="h-4 w-4" />
<span>{urgentTasks.length} {t('adhd.hyperfocus.urgentTasks')}</span>
</div>
)}
</div>
{/* Urgent tasks preview */}
{urgentTasks.length > 0 && (
<div className="mb-4 space-y-2">
<p className="text-xs font-medium text-amber-800 dark:text-amber-200 mb-2">
{t('adhd.hyperfocus.waitingTasks')}:
</p>
{urgentTasks.slice(0, 3).map((task) => (
<button
key={task.id}
onClick={() => handleSwitchTask(task.id)}
className={`
w-full text-left p-2 rounded-lg
bg-amber-100 dark:bg-amber-900/50
hover:bg-amber-200 dark:hover:bg-amber-900
transition-colors text-sm
text-amber-900 dark:text-amber-100
`}
>
{task.title}
</button>
))}
</div>
)}
{/* Actions */}
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleTakeBreak}
className="flex-1 border-amber-500/50 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/50"
>
<Coffee className="h-4 w-4 mr-2" />
{t('adhd.hyperfocus.takeBreak')}
</Button>
<Button
onClick={handleDismiss}
className="flex-1 bg-amber-500 hover:bg-amber-600 text-white"
>
{t('adhd.hyperfocus.continue')}
</Button>
</div>
{/* Gentle reminder */}
<p className="text-xs text-center text-amber-600 dark:text-amber-400 mt-3 italic">
{t('adhd.hyperfocus.gentle')}
</p>
</div>
</motion.div>
</AnimatePresence>
);
}
@@ -0,0 +1,213 @@
import React, { useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion, AnimatePresence } from 'framer-motion';
import { Zap, Clock, Star, CheckCircle2, Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { useTranslation } from 'react-i18next';
import { useADHDMode } from '../providers/ADHDModeProvider';
import { useEncouragement, EncouragementToast } from '../feedback/EncouragementToast';
import type { Task } from '@shared/schema';
import confetti from 'canvas-confetti';
interface QuickWinListProps {
maxItems?: number;
compact?: boolean;
}
function calculateQuickWinScore(task: Task, threshold: number): number {
let score = 100;
// Shorter tasks = higher score
if (task.estimatedDuration) {
score += Math.max(0, (threshold - task.estimatedDuration) * 10);
} else {
// No estimate - assume quick
score += 30;
}
// Lower priority = quicker to complete (less mental load)
if (task.priority === 'low') score += 20;
if (task.priority === 'medium') score += 10;
// Today's due date = bonus
if (task.dueDate) {
const today = new Date();
const dueDate = new Date(task.dueDate);
if (
dueDate.getDate() === today.getDate() &&
dueDate.getMonth() === today.getMonth() &&
dueDate.getFullYear() === today.getFullYear()
) {
score += 30;
}
}
// Already in progress = bonus
if (task.status === 'inProgress') score += 40;
// Low energy level tasks are good quick wins
if (task.energyLevel === 'low') score += 25;
return score;
}
export function QuickWinList({ maxItems = 5, compact = false }: QuickWinListProps) {
const { t } = useTranslation();
const { settings } = useADHDMode();
const queryClient = useQueryClient();
const encouragement = useEncouragement();
const { data: tasks = [] } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
const quickWins = useMemo(() => {
const threshold = settings.quickWinThreshold || 10;
return tasks
.filter((task) => {
// Only pending or in-progress tasks
if (task.status === 'done') return false;
// Only tasks without subtasks (leaf tasks)
if (tasks.some((t) => t.parentTaskId === task.id)) return false;
// Filter by duration threshold
if (task.estimatedDuration && task.estimatedDuration > threshold * 1.5) return false;
return true;
})
.map((task) => ({
...task,
quickWinScore: calculateQuickWinScore(task, threshold),
}))
.sort((a, b) => b.quickWinScore - a.quickWinScore)
.slice(0, maxItems);
}, [tasks, maxItems, settings.quickWinThreshold]);
// Mutation to complete task
const completeMutation = useMutation({
mutationFn: async (taskId: string) => {
const res = await fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ status: 'done' }),
});
if (!res.ok) throw new Error('Failed to complete task');
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
queryClient.invalidateQueries({ queryKey: ['/api/user'] });
// Celebrate!
confetti({
particleCount: 50,
spread: 60,
origin: { y: 0.8 },
colors: ['#10b981', '#3b82f6', '#8b5cf6'],
});
encouragement.show('quickWin', undefined, 30);
},
});
const handleComplete = (taskId: string) => {
completeMutation.mutate(taskId);
};
if (quickWins.length === 0) {
return (
<Card className={compact ? 'border-0 shadow-none' : ''}>
<CardContent className="flex flex-col items-center justify-center py-8 text-center">
<Sparkles className="h-12 w-12 text-muted-foreground/50 mb-4" />
<p className="text-muted-foreground">
{t('adhd.quickWins.noTasks')}
</p>
</CardContent>
</Card>
);
}
return (
<>
<Card className={compact ? 'border-0 shadow-none' : ''}>
{!compact && (
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg">
<Zap className="h-5 w-5 text-yellow-500" />
{t('adhd.quickWins.title')}
</CardTitle>
<CardDescription>
{t('adhd.quickWins.description')}
</CardDescription>
</CardHeader>
)}
<CardContent className={compact ? 'p-0' : ''}>
<div className="space-y-2">
<AnimatePresence mode="popLayout">
{quickWins.map((task, index) => (
<motion.div
key={task.id}
layout
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20, height: 0 }}
transition={{ delay: index * 0.05 }}
className={`
flex items-center gap-3 p-3 rounded-lg
bg-gradient-to-r from-yellow-500/5 to-orange-500/5
border border-yellow-500/20
hover:border-yellow-500/40 transition-colors
${settings.largerTargets ? 'min-h-[72px]' : ''}
`}
>
<Button
variant="ghost"
size="icon"
onClick={() => handleComplete(task.id)}
disabled={completeMutation.isPending}
className={`
shrink-0 rounded-full
hover:bg-green-500/20 hover:text-green-500
${settings.largerTargets ? 'h-12 w-12' : 'h-9 w-9'}
`}
>
<CheckCircle2 className={settings.largerTargets ? 'h-6 w-6' : 'h-5 w-5'} />
</Button>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{task.title}</p>
<div className="flex items-center gap-2 mt-1">
{task.estimatedDuration && (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
{task.estimatedDuration} min
</span>
)}
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
+30 XP
</Badge>
</div>
</div>
<div className="shrink-0">
<Star className="h-4 w-4 text-yellow-500 fill-yellow-500" />
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</CardContent>
</Card>
<EncouragementToast
type={encouragement.type}
visible={encouragement.visible}
onDismiss={encouragement.hide}
customMessage={encouragement.message}
xpEarned={encouragement.xp}
/>
</>
);
}
@@ -0,0 +1,320 @@
import React, { useState, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion, AnimatePresence } from 'framer-motion';
import {
ChevronLeft,
ChevronRight,
CheckCircle2,
Clock,
Play,
SkipForward,
Focus,
Sparkles,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { useTranslation } from 'react-i18next';
import { useADHDMode } from '../providers/ADHDModeProvider';
import { VisualTimer } from '../timer/VisualTimer';
import { FiveMinuteStarter } from './FiveMinuteStarter';
import { useEncouragement, EncouragementToast } from '../feedback/EncouragementToast';
import type { Task } from '@shared/schema';
import confetti from 'canvas-confetti';
interface SingleTaskViewProps {
onExit?: () => void;
}
export function SingleTaskView({ onExit }: SingleTaskViewProps) {
const { t } = useTranslation();
const { settings } = useADHDMode();
const queryClient = useQueryClient();
const encouragement = useEncouragement();
const [currentIndex, setCurrentIndex] = useState(0);
const [showTimer, setShowTimer] = useState(false);
const [showFiveMin, setShowFiveMin] = useState(false);
const { data: tasks = [] } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
// Filter to actionable tasks (not done, no subtasks)
const actionableTasks = useMemo(() => {
return tasks.filter((task) => {
if (task.status === 'done') return false;
// Exclude parent tasks (tasks with subtasks)
if (tasks.some((t) => t.parentTaskId === task.id)) return false;
return true;
});
}, [tasks]);
const currentTask = actionableTasks[currentIndex];
const totalTasks = actionableTasks.length;
// Complete task mutation
const completeMutation = useMutation({
mutationFn: async (taskId: string) => {
const res = await fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ status: 'done' }),
});
if (!res.ok) throw new Error('Failed to complete task');
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
queryClient.invalidateQueries({ queryKey: ['/api/user'] });
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
});
encouragement.show('taskComplete', undefined, 50);
// Move to next task if available
if (currentIndex >= actionableTasks.length - 1) {
setCurrentIndex(Math.max(0, actionableTasks.length - 2));
}
},
});
const handleComplete = () => {
if (currentTask) {
completeMutation.mutate(currentTask.id);
}
};
const handleNext = () => {
if (currentIndex < totalTasks - 1) {
setCurrentIndex(currentIndex + 1);
}
};
const handlePrev = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
}
};
const handleSkip = () => {
handleNext();
};
if (totalTasks === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] p-8 text-center">
<Sparkles className="h-16 w-16 text-primary mb-6" />
<h2 className="text-2xl font-bold mb-2">{t('adhd.singleTask.allDone')}</h2>
<p className="text-muted-foreground mb-6">{t('adhd.singleTask.allDoneDesc')}</p>
{onExit && (
<Button onClick={onExit} size="lg">
{t('adhd.singleTask.exit')}
</Button>
)}
</div>
);
}
return (
<>
<div className="flex flex-col items-center justify-center min-h-[70vh] p-6">
{/* Progress indicator */}
<div className="w-full max-w-md mb-8">
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
<span>{t('adhd.singleTask.task')} {currentIndex + 1} / {totalTasks}</span>
<span>{Math.round(((totalTasks - actionableTasks.length + (tasks.length - actionableTasks.length)) / tasks.length) * 100)}% {t('adhd.singleTask.complete')}</span>
</div>
<Progress value={((currentIndex + 1) / totalTasks) * 100} className="h-2" />
</div>
{/* Timer (when active) */}
<AnimatePresence>
{showTimer && currentTask?.estimatedDuration && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="mb-8"
>
<VisualTimer
duration={currentTask.estimatedDuration * 60}
onComplete={() => setShowTimer(false)}
taskTitle={currentTask.title}
size="medium"
/>
</motion.div>
)}
</AnimatePresence>
{/* 5-Minute Starter */}
<AnimatePresence>
{showFiveMin && currentTask && (
<FiveMinuteStarter
task={currentTask}
onClose={() => setShowFiveMin(false)}
onComplete={handleComplete}
/>
)}
</AnimatePresence>
{/* Task Card */}
{!showTimer && !showFiveMin && (
<AnimatePresence mode="wait">
<motion.div
key={currentTask?.id}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: settings.reducedAnimations ? 0 : 0.3 }}
className="w-full max-w-lg"
>
<div className="bg-card rounded-2xl border shadow-lg p-8">
{/* Priority badge */}
{currentTask && (
<div className="flex items-center justify-between mb-4">
<Badge
variant={
currentTask.priority === 'high'
? 'destructive'
: currentTask.priority === 'low'
? 'secondary'
: 'default'
}
>
{t(`tasks.priority.${currentTask.priority}`)}
</Badge>
{currentTask.estimatedDuration && (
<span className="flex items-center gap-1 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
{currentTask.estimatedDuration} min
</span>
)}
</div>
)}
{/* Task title */}
<h2 className="text-2xl md:text-3xl font-bold mb-4 text-center">
{currentTask?.title}
</h2>
{/* Task description */}
{currentTask?.description && (
<p className="text-muted-foreground text-center mb-6">
{currentTask.description}
</p>
)}
{/* Action buttons */}
<div className="flex flex-col gap-3">
<Button
size="lg"
onClick={handleComplete}
disabled={completeMutation.isPending}
className={`w-full ${settings.largerTargets ? 'h-14 text-lg' : 'h-12'}`}
>
<CheckCircle2 className="h-5 w-5 mr-2" />
{t('adhd.singleTask.markComplete')}
</Button>
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => setShowFiveMin(true)}
className={`flex-1 ${settings.largerTargets ? 'h-12' : 'h-10'}`}
>
<Play className="h-4 w-4 mr-2" />
{t('adhd.singleTask.fiveMin')}
</Button>
{currentTask?.estimatedDuration && (
<Button
variant="outline"
onClick={() => setShowTimer(true)}
className={`flex-1 ${settings.largerTargets ? 'h-12' : 'h-10'}`}
>
<Focus className="h-4 w-4 mr-2" />
{t('adhd.singleTask.startTimer')}
</Button>
)}
</div>
<Button
variant="ghost"
onClick={handleSkip}
disabled={currentIndex >= totalTasks - 1}
className="text-muted-foreground"
>
<SkipForward className="h-4 w-4 mr-2" />
{t('adhd.singleTask.skip')}
</Button>
</div>
</div>
</motion.div>
</AnimatePresence>
)}
{/* Navigation dots */}
<div className="flex items-center gap-4 mt-8">
<Button
variant="ghost"
size="icon"
onClick={handlePrev}
disabled={currentIndex === 0}
>
<ChevronLeft className="h-5 w-5" />
</Button>
<div className="flex gap-1.5">
{actionableTasks.slice(0, 7).map((_, index) => (
<button
key={index}
onClick={() => setCurrentIndex(index)}
className={`
w-2.5 h-2.5 rounded-full transition-all
${index === currentIndex ? 'bg-primary w-6' : 'bg-muted hover:bg-muted-foreground/50'}
`}
/>
))}
{totalTasks > 7 && (
<span className="text-xs text-muted-foreground ml-1">+{totalTasks - 7}</span>
)}
</div>
<Button
variant="ghost"
size="icon"
onClick={handleNext}
disabled={currentIndex >= totalTasks - 1}
>
<ChevronRight className="h-5 w-5" />
</Button>
</div>
{/* Exit button */}
{onExit && (
<Button
variant="link"
onClick={onExit}
className="mt-6 text-muted-foreground"
>
{t('adhd.singleTask.exit')}
</Button>
)}
</div>
<EncouragementToast
type={encouragement.type}
visible={encouragement.visible}
onDismiss={encouragement.hide}
customMessage={encouragement.message}
xpEarned={encouragement.xp}
/>
</>
);
}
+29
View File
@@ -0,0 +1,29 @@
// Providers
export { ADHDModeProvider, useADHDMode, defaultADHDSettings } from './providers/ADHDModeProvider';
export { BreakReminderProvider, useBreakReminder } from './providers/BreakReminderProvider';
// Toggle
export { ADHDModeToggle } from './ADHDModeToggle';
// Timer Components
export { VisualTimer } from './timer/VisualTimer';
// Focus Components
export { QuickWinList } from './focus/QuickWinList';
export { SingleTaskView } from './focus/SingleTaskView';
export { FiveMinuteStarter } from './focus/FiveMinuteStarter';
export { HyperfocusGuard } from './focus/HyperfocusGuard';
// AI Components
export { TaskBreakdownModal } from './ai/TaskBreakdownModal';
// Feedback Components
export { BreakReminderOverlay } from './feedback/BreakReminderOverlay';
export { EncouragementToast, useEncouragement } from './feedback/EncouragementToast';
// Social Components
export { BodyDoublingLobby } from './social/BodyDoublingLobby';
// Settings Components
export { ADHDSettingsPanel } from './settings/ADHDSettingsPanel';
export { EnergyCheckIn } from './settings/EnergyCheckIn';
@@ -0,0 +1,122 @@
import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { ADHDSettings } from '@shared/schema';
const defaultADHDSettings: ADHDSettings = {
reducedAnimations: false,
largerTargets: true,
breakReminderInterval: 45,
singleTaskModeDefault: false,
positiveMessagingLevel: 'normal',
hyperfocusProtection: true,
hyperfocusMaxMinutes: 90,
visualTimerEnabled: true,
quickWinThreshold: 10,
energyCheckInsEnabled: true,
};
interface ADHDModeContextType {
isEnabled: boolean;
settings: ADHDSettings;
toggle: () => void;
updateSettings: (settings: Partial<ADHDSettings>) => void;
isLoading: boolean;
}
const ADHDModeContext = createContext<ADHDModeContextType | undefined>(undefined);
export function ADHDModeProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient();
// Fetch user data to get ADHD settings
const { data: user, isLoading } = useQuery<{
adhdMode: boolean;
adhdSettings: ADHDSettings;
}>({
queryKey: ['/api/user'],
});
const [localEnabled, setLocalEnabled] = useState(false);
const [localSettings, setLocalSettings] = useState<ADHDSettings>(defaultADHDSettings);
// Sync with server data
useEffect(() => {
if (user) {
setLocalEnabled(user.adhdMode ?? false);
setLocalSettings(user.adhdSettings ?? defaultADHDSettings);
}
}, [user]);
// Apply ADHD mode class to document
useEffect(() => {
const root = document.documentElement;
if (localEnabled) {
root.classList.add('adhd-mode');
if (localSettings.reducedAnimations) {
root.classList.add('reduced-motion');
} else {
root.classList.remove('reduced-motion');
}
if (localSettings.largerTargets) {
root.classList.add('larger-targets');
} else {
root.classList.remove('larger-targets');
}
} else {
root.classList.remove('adhd-mode', 'reduced-motion', 'larger-targets');
}
}, [localEnabled, localSettings]);
// Mutation to update ADHD settings
const updateMutation = useMutation({
mutationFn: async (data: { adhdMode: boolean; adhdSettings: ADHDSettings }) => {
const res = await fetch('/api/user/adhd-settings', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to update ADHD settings');
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/user'] });
},
});
const toggle = useCallback(() => {
const newEnabled = !localEnabled;
setLocalEnabled(newEnabled);
updateMutation.mutate({ adhdMode: newEnabled, adhdSettings: localSettings });
}, [localEnabled, localSettings, updateMutation]);
const updateSettings = useCallback((newSettings: Partial<ADHDSettings>) => {
const merged = { ...localSettings, ...newSettings };
setLocalSettings(merged);
updateMutation.mutate({ adhdMode: localEnabled, adhdSettings: merged });
}, [localEnabled, localSettings, updateMutation]);
return (
<ADHDModeContext.Provider
value={{
isEnabled: localEnabled,
settings: localSettings,
toggle,
updateSettings,
isLoading,
}}
>
{children}
</ADHDModeContext.Provider>
);
}
export function useADHDMode() {
const context = useContext(ADHDModeContext);
if (context === undefined) {
throw new Error('useADHDMode must be used within an ADHDModeProvider');
}
return context;
}
export { defaultADHDSettings };
@@ -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;
}
@@ -0,0 +1,297 @@
import React from 'react';
import { useADHDMode, defaultADHDSettings } from '../providers/ADHDModeProvider';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import {
Brain,
Timer,
Sparkles,
Coffee,
Target,
Battery,
Zap,
Shield,
RotateCcw,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { ADHDModeToggle } from '../ADHDModeToggle';
export function ADHDSettingsPanel() {
const { t } = useTranslation();
const { isEnabled, settings, updateSettings } = useADHDMode();
const handleReset = () => {
updateSettings(defaultADHDSettings);
};
return (
<div className="space-y-6">
{/* Main Toggle */}
<ADHDModeToggle showLabel={true} />
{/* Settings (only shown when enabled) */}
{isEnabled && (
<>
{/* Visual & Interaction Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Target className="h-5 w-5" />
{t('adhd.settings.visualTitle')}
</CardTitle>
<CardDescription>
{t('adhd.settings.visualDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Reduced Animations */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">{t('adhd.settings.reducedAnimations')}</Label>
<p className="text-sm text-muted-foreground">
{t('adhd.settings.reducedAnimationsDesc')}
</p>
</div>
<Switch
checked={settings.reducedAnimations}
onCheckedChange={(checked) => updateSettings({ reducedAnimations: checked })}
/>
</div>
{/* Larger Touch Targets */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">{t('adhd.settings.largerTargets')}</Label>
<p className="text-sm text-muted-foreground">
{t('adhd.settings.largerTargetsDesc')}
</p>
</div>
<Switch
checked={settings.largerTargets}
onCheckedChange={(checked) => updateSettings({ largerTargets: checked })}
/>
</div>
{/* Single Task Mode Default */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">{t('adhd.settings.singleTaskDefault')}</Label>
<p className="text-sm text-muted-foreground">
{t('adhd.settings.singleTaskDefaultDesc')}
</p>
</div>
<Switch
checked={settings.singleTaskModeDefault}
onCheckedChange={(checked) => updateSettings({ singleTaskModeDefault: checked })}
/>
</div>
</CardContent>
</Card>
{/* Timer & Focus Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Timer className="h-5 w-5" />
{t('adhd.settings.timerTitle')}
</CardTitle>
<CardDescription>
{t('adhd.settings.timerDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Visual Timer */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">{t('adhd.settings.visualTimer')}</Label>
<p className="text-sm text-muted-foreground">
{t('adhd.settings.visualTimerDesc')}
</p>
</div>
<Switch
checked={settings.visualTimerEnabled}
onCheckedChange={(checked) => updateSettings({ visualTimerEnabled: checked })}
/>
</div>
{/* Hyperfocus Protection */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base flex items-center gap-2">
<Shield className="h-4 w-4" />
{t('adhd.settings.hyperfocusProtection')}
</Label>
<p className="text-sm text-muted-foreground">
{t('adhd.settings.hyperfocusProtectionDesc')}
</p>
</div>
<Switch
checked={settings.hyperfocusProtection}
onCheckedChange={(checked) => updateSettings({ hyperfocusProtection: checked })}
/>
</div>
{/* Hyperfocus Max Minutes */}
{settings.hyperfocusProtection && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>{t('adhd.settings.hyperfocusMaxMinutes')}</Label>
<span className="text-sm text-muted-foreground">
{settings.hyperfocusMaxMinutes} min
</span>
</div>
<Slider
value={[settings.hyperfocusMaxMinutes]}
onValueChange={([value]) => updateSettings({ hyperfocusMaxMinutes: value })}
min={30}
max={180}
step={15}
className="w-full"
/>
</div>
)}
</CardContent>
</Card>
{/* Break Reminder Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Coffee className="h-5 w-5" />
{t('adhd.settings.breakTitle')}
</CardTitle>
<CardDescription>
{t('adhd.settings.breakDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Break Reminder Interval */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>{t('adhd.settings.breakInterval')}</Label>
<span className="text-sm text-muted-foreground">
{settings.breakReminderInterval} min
</span>
</div>
<Slider
value={[settings.breakReminderInterval]}
onValueChange={([value]) => updateSettings({ breakReminderInterval: value })}
min={15}
max={90}
step={5}
className="w-full"
/>
</div>
</CardContent>
</Card>
{/* Motivation Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Sparkles className="h-5 w-5" />
{t('adhd.settings.motivationTitle')}
</CardTitle>
<CardDescription>
{t('adhd.settings.motivationDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Positive Messaging Level */}
<div className="space-y-2">
<Label>{t('adhd.settings.messagingLevel')}</Label>
<Select
value={settings.positiveMessagingLevel}
onValueChange={(value: 'minimal' | 'normal' | 'high') =>
updateSettings({ positiveMessagingLevel: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="minimal">{t('adhd.settings.messagingMinimal')}</SelectItem>
<SelectItem value="normal">{t('adhd.settings.messagingNormal')}</SelectItem>
<SelectItem value="high">{t('adhd.settings.messagingHigh')}</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t('adhd.settings.messagingLevelDesc')}
</p>
</div>
{/* Quick Win Threshold */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Zap className="h-4 w-4 text-yellow-500" />
{t('adhd.settings.quickWinThreshold')}
</Label>
<span className="text-sm text-muted-foreground">
{settings.quickWinThreshold} min
</span>
</div>
<Slider
value={[settings.quickWinThreshold]}
onValueChange={([value]) => updateSettings({ quickWinThreshold: value })}
min={5}
max={30}
step={5}
className="w-full"
/>
<p className="text-xs text-muted-foreground">
{t('adhd.settings.quickWinThresholdDesc')}
</p>
</div>
</CardContent>
</Card>
{/* Energy Check-ins */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Battery className="h-5 w-5" />
{t('adhd.settings.energyTitle')}
</CardTitle>
<CardDescription>
{t('adhd.settings.energyDesc')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">{t('adhd.settings.energyCheckIns')}</Label>
<p className="text-sm text-muted-foreground">
{t('adhd.settings.energyCheckInsDesc')}
</p>
</div>
<Switch
checked={settings.energyCheckInsEnabled}
onCheckedChange={(checked) => updateSettings({ energyCheckInsEnabled: checked })}
/>
</div>
</CardContent>
</Card>
{/* Reset Button */}
<div className="flex justify-end">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="h-4 w-4 mr-2" />
{t('adhd.settings.reset')}
</Button>
</div>
</>
)}
</div>
);
}
@@ -0,0 +1,159 @@
import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { motion, AnimatePresence } from 'framer-motion';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Battery, BatteryLow, BatteryMedium, BatteryFull, Sparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useADHDMode } from '../providers/ADHDModeProvider';
interface EnergyCheckInProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onComplete?: (level: string) => void;
}
type EnergyLevel = 'low' | 'medium' | 'high';
const energyLevels: { id: EnergyLevel; icon: React.ComponentType<{ className?: string }>; color: string }[] = [
{ id: 'low', icon: BatteryLow, color: 'text-red-500 bg-red-500/10 border-red-500/30 hover:bg-red-500/20' },
{ id: 'medium', icon: BatteryMedium, color: 'text-yellow-500 bg-yellow-500/10 border-yellow-500/30 hover:bg-yellow-500/20' },
{ id: 'high', icon: BatteryFull, color: 'text-green-500 bg-green-500/10 border-green-500/30 hover:bg-green-500/20' },
];
export function EnergyCheckIn({ open, onOpenChange, onComplete }: EnergyCheckInProps) {
const { t } = useTranslation();
const { settings } = useADHDMode();
const queryClient = useQueryClient();
const [selectedLevel, setSelectedLevel] = useState<EnergyLevel | null>(null);
const [notes, setNotes] = useState('');
const logMutation = useMutation({
mutationFn: async ({ energyLevel, notes }: { energyLevel: string; notes?: string }) => {
const res = await fetch('/api/energy/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ energyLevel, notes }),
});
if (!res.ok) throw new Error('Failed to log energy');
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/user'] });
onComplete?.(selectedLevel!);
onOpenChange(false);
setSelectedLevel(null);
setNotes('');
},
});
const handleSubmit = () => {
if (!selectedLevel) return;
logMutation.mutate({ energyLevel: selectedLevel, notes: notes || undefined });
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Battery className="h-5 w-5 text-primary" />
{t('adhd.energy.title')}
</DialogTitle>
<DialogDescription>
{t('adhd.energy.description')}
</DialogDescription>
</DialogHeader>
<div className="py-4">
{/* Energy level selection */}
<div className="grid grid-cols-3 gap-3 mb-6">
{energyLevels.map((level) => {
const Icon = level.icon;
const isSelected = selectedLevel === level.id;
return (
<motion.button
key={level.id}
whileHover={{ scale: settings.reducedAnimations ? 1 : 1.02 }}
whileTap={{ scale: settings.reducedAnimations ? 1 : 0.98 }}
onClick={() => setSelectedLevel(level.id)}
className={`
flex flex-col items-center gap-2 p-4 rounded-xl border-2
transition-all duration-200
${level.color}
${isSelected ? 'ring-2 ring-offset-2 ring-primary' : ''}
${settings.largerTargets ? 'min-h-[100px]' : 'min-h-[80px]'}
`}
>
<Icon className="h-8 w-8" />
<span className="font-medium text-sm">
{t(`adhd.energy.levels.${level.id}`)}
</span>
</motion.button>
);
})}
</div>
{/* Energy-based suggestions */}
<AnimatePresence>
{selectedLevel && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mb-4"
>
<div className="bg-muted/50 rounded-lg p-4">
<div className="flex items-start gap-2">
<Sparkles className="h-4 w-4 text-primary mt-0.5" />
<div>
<p className="font-medium text-sm mb-1">
{t(`adhd.energy.suggestions.${selectedLevel}.title`)}
</p>
<p className="text-xs text-muted-foreground">
{t(`adhd.energy.suggestions.${selectedLevel}.description`)}
</p>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Optional notes */}
<div className="mb-4">
<label className="text-sm font-medium mb-2 block">
{t('adhd.energy.notesLabel')} <span className="text-muted-foreground">({t('common.optional')})</span>
</label>
<Textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder={t('adhd.energy.notesPlaceholder')}
rows={2}
className="resize-none"
/>
</div>
{/* Submit button */}
<Button
onClick={handleSubmit}
disabled={!selectedLevel || logMutation.isPending}
className={`w-full ${settings.largerTargets ? 'h-12' : 'h-10'}`}
>
{logMutation.isPending ? t('common.saving') : t('adhd.energy.submit')}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,380 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion, AnimatePresence } from 'framer-motion';
import {
Users,
Plus,
Calendar,
Clock,
Play,
UserPlus,
LogOut,
Crown,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useTranslation } from 'react-i18next';
import { useADHDMode } from '../providers/ADHDModeProvider';
import { format } from 'date-fns';
import type { BodyDoublingSession } from '@shared/schema';
interface SessionWithParticipants extends BodyDoublingSession {
participantCount: number;
isParticipant: boolean;
host: { username: string };
}
const sessionTypes = [
{ id: 'focus', icon: '🎯' },
{ id: 'brainstorm', icon: '💡' },
{ id: 'admin', icon: '📋' },
{ id: 'creative', icon: '🎨' },
];
export function BodyDoublingLobby() {
const { t } = useTranslation();
const { settings } = useADHDMode();
const queryClient = useQueryClient();
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [newSession, setNewSession] = useState({
title: '',
sessionType: 'focus',
durationMinutes: 50,
startsAt: '',
});
// Fetch sessions
const { data: sessions = [], isLoading } = useQuery<SessionWithParticipants[]>({
queryKey: ['/api/sessions'],
});
// Create session mutation
const createMutation = useMutation({
mutationFn: async (data: typeof newSession) => {
const res = await fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
...data,
startsAt: new Date(data.startsAt).toISOString(),
}),
});
if (!res.ok) throw new Error('Failed to create session');
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/sessions'] });
setCreateDialogOpen(false);
setNewSession({
title: '',
sessionType: 'focus',
durationMinutes: 50,
startsAt: '',
});
},
});
// Join session mutation
const joinMutation = useMutation({
mutationFn: async (sessionId: string) => {
const res = await fetch(`/api/sessions/${sessionId}/join`, {
method: 'POST',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to join session');
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/sessions'] });
},
});
// Leave session mutation
const leaveMutation = useMutation({
mutationFn: async (sessionId: string) => {
const res = await fetch(`/api/sessions/${sessionId}/leave`, {
method: 'POST',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to leave session');
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/sessions'] });
},
});
const handleCreate = () => {
createMutation.mutate(newSession);
};
const upcomingSessions = sessions.filter((s) => s.status === 'scheduled');
const activeSessions = sessions.filter((s) => s.status === 'active');
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Users className="h-6 w-6 text-primary" />
{t('adhd.bodyDoubling.title')}
</h2>
<p className="text-muted-foreground">
{t('adhd.bodyDoubling.description')}
</p>
</div>
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<DialogTrigger asChild>
<Button className={settings.largerTargets ? 'h-12 px-6' : ''}>
<Plus className="h-4 w-4 mr-2" />
{t('adhd.bodyDoubling.createSession')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('adhd.bodyDoubling.newSession')}</DialogTitle>
<DialogDescription>
{t('adhd.bodyDoubling.newSessionDesc')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="title">{t('adhd.bodyDoubling.sessionTitle')}</Label>
<Input
id="title"
value={newSession.title}
onChange={(e) => setNewSession({ ...newSession, title: e.target.value })}
placeholder={t('adhd.bodyDoubling.titlePlaceholder')}
/>
</div>
<div>
<Label>{t('adhd.bodyDoubling.sessionType')}</Label>
<Select
value={newSession.sessionType}
onValueChange={(value) => setNewSession({ ...newSession, sessionType: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{sessionTypes.map((type) => (
<SelectItem key={type.id} value={type.id}>
{type.icon} {t(`adhd.bodyDoubling.types.${type.id}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="startsAt">{t('adhd.bodyDoubling.startTime')}</Label>
<Input
id="startsAt"
type="datetime-local"
value={newSession.startsAt}
onChange={(e) => setNewSession({ ...newSession, startsAt: e.target.value })}
/>
</div>
<div>
<Label>{t('adhd.bodyDoubling.duration')}</Label>
<Select
value={newSession.durationMinutes.toString()}
onValueChange={(value) => setNewSession({ ...newSession, durationMinutes: parseInt(value) })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="25">25 min</SelectItem>
<SelectItem value="50">50 min</SelectItem>
<SelectItem value="90">90 min</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button
onClick={handleCreate}
disabled={!newSession.title || !newSession.startsAt || createMutation.isPending}
className="w-full"
>
{createMutation.isPending ? t('common.creating') : t('adhd.bodyDoubling.create')}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
{/* Active Sessions */}
{activeSessions.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
<Play className="h-5 w-5 text-green-500" />
{t('adhd.bodyDoubling.activeSessions')}
</h3>
<div className="grid gap-4 md:grid-cols-2">
{activeSessions.map((session) => (
<SessionCard
key={session.id}
session={session}
onJoin={() => joinMutation.mutate(session.id)}
onLeave={() => leaveMutation.mutate(session.id)}
isJoining={joinMutation.isPending}
isLeaving={leaveMutation.isPending}
/>
))}
</div>
</div>
)}
{/* Upcoming Sessions */}
<div>
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
<Calendar className="h-5 w-5 text-primary" />
{t('adhd.bodyDoubling.upcomingSessions')}
</h3>
{upcomingSessions.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-8">
<Users className="h-12 w-12 text-muted-foreground/50 mb-4" />
<p className="text-muted-foreground text-center">
{t('adhd.bodyDoubling.noSessions')}
</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<AnimatePresence>
{upcomingSessions.map((session, index) => (
<motion.div
key={session.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<SessionCard
session={session}
onJoin={() => joinMutation.mutate(session.id)}
onLeave={() => leaveMutation.mutate(session.id)}
isJoining={joinMutation.isPending}
isLeaving={leaveMutation.isPending}
/>
</motion.div>
))}
</AnimatePresence>
</div>
)}
</div>
</div>
);
}
// Session Card Component
function SessionCard({
session,
onJoin,
onLeave,
isJoining,
isLeaving,
}: {
session: SessionWithParticipants;
onJoin: () => void;
onLeave: () => void;
isJoining: boolean;
isLeaving: boolean;
}) {
const { t } = useTranslation();
const { settings } = useADHDMode();
const sessionType = sessionTypes.find((t) => t.id === session.sessionType);
const isActive = session.status === 'active';
return (
<Card className={`
${isActive ? 'border-green-500/50 bg-green-500/5' : ''}
${session.isParticipant ? 'ring-2 ring-primary/50' : ''}
`}>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<span className="text-2xl">{sessionType?.icon}</span>
<div>
<CardTitle className="text-base">{session.title}</CardTitle>
<CardDescription className="flex items-center gap-1">
<Crown className="h-3 w-3" />
{session.host?.username}
</CardDescription>
</div>
</div>
{isActive && (
<Badge variant="default" className="bg-green-500">
{t('adhd.bodyDoubling.live')}
</Badge>
)}
</div>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4 text-sm text-muted-foreground mb-4">
<span className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{format(new Date(session.startsAt), 'MMM d, HH:mm')}
</span>
<span className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{session.durationMinutes} min
</span>
<span className="flex items-center gap-1">
<Users className="h-4 w-4" />
{session.participantCount}/{session.maxParticipants}
</span>
</div>
{session.isParticipant ? (
<Button
variant="outline"
onClick={onLeave}
disabled={isLeaving}
className={`w-full ${settings.largerTargets ? 'h-11' : ''}`}
>
<LogOut className="h-4 w-4 mr-2" />
{t('adhd.bodyDoubling.leave')}
</Button>
) : (
<Button
onClick={onJoin}
disabled={isJoining || session.participantCount >= session.maxParticipants}
className={`w-full ${settings.largerTargets ? 'h-11' : ''}`}
>
<UserPlus className="h-4 w-4 mr-2" />
{t('adhd.bodyDoubling.join')}
</Button>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,326 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Play, Pause, RotateCcw, Volume2, VolumeX, Maximize2, Minimize2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTranslation } from 'react-i18next';
import { useADHDMode } from '../providers/ADHDModeProvider';
interface VisualTimerProps {
duration: number; // in seconds
onComplete?: () => void;
onMilestone?: (percent: number) => void;
colorScheme?: 'default' | 'calm' | 'energetic';
showNumbers?: boolean;
size?: 'small' | 'medium' | 'large' | 'fullscreen';
autoStart?: boolean;
taskTitle?: string;
}
const colorSchemes = {
default: {
start: 'hsl(142, 76%, 36%)',
mid: 'hsl(45, 93%, 47%)',
end: 'hsl(0, 84%, 60%)',
bg: 'hsl(0, 0%, 90%)',
},
calm: {
start: 'hsl(200, 70%, 50%)',
mid: 'hsl(180, 60%, 45%)',
end: 'hsl(280, 60%, 55%)',
bg: 'hsl(220, 20%, 95%)',
},
energetic: {
start: 'hsl(160, 84%, 39%)',
mid: 'hsl(38, 92%, 50%)',
end: 'hsl(350, 89%, 60%)',
bg: 'hsl(0, 0%, 92%)',
},
};
const sizes = {
small: { ring: 120, stroke: 8, font: 'text-xl' },
medium: { ring: 200, stroke: 12, font: 'text-3xl' },
large: { ring: 300, stroke: 16, font: 'text-5xl' },
fullscreen: { ring: 400, stroke: 20, font: 'text-7xl' },
};
export function VisualTimer({
duration,
onComplete,
onMilestone,
colorScheme = 'default',
showNumbers = true,
size = 'medium',
autoStart = false,
taskTitle,
}: VisualTimerProps) {
const { t } = useTranslation();
const { settings } = useADHDMode();
const [timeLeft, setTimeLeft] = useState(duration);
const [isRunning, setIsRunning] = useState(autoStart);
const [isFullscreen, setIsFullscreen] = useState(false);
const [soundEnabled, setSoundEnabled] = useState(true);
const [lastMilestone, setLastMilestone] = useState(100);
const audioRef = useRef<HTMLAudioElement | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const colors = colorSchemes[colorScheme];
const sizeConfig = isFullscreen ? sizes.fullscreen : sizes[size];
const progress = timeLeft / duration;
const percent = Math.round(progress * 100);
// Calculate current color based on progress
const getCurrentColor = useCallback(() => {
if (progress > 0.5) {
return colors.start;
} else if (progress > 0.25) {
return colors.mid;
} else {
return colors.end;
}
}, [progress, colors]);
// Format time display
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
// Play sound effect
const playSound = useCallback((type: 'tick' | 'milestone' | 'complete') => {
if (!soundEnabled) return;
// Simple beep sounds using Web Audio API
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
switch (type) {
case 'tick':
oscillator.frequency.value = 800;
gainNode.gain.value = 0.1;
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.05);
break;
case 'milestone':
oscillator.frequency.value = 600;
gainNode.gain.value = 0.2;
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.2);
break;
case 'complete':
oscillator.frequency.value = 440;
gainNode.gain.value = 0.3;
oscillator.start();
setTimeout(() => {
oscillator.frequency.value = 554;
}, 200);
setTimeout(() => {
oscillator.frequency.value = 659;
}, 400);
oscillator.stop(audioContext.currentTime + 0.6);
break;
}
}, [soundEnabled]);
// Timer logic
useEffect(() => {
if (!isRunning || timeLeft <= 0) return;
const interval = setInterval(() => {
setTimeLeft((prev) => {
const newTime = prev - 1;
if (newTime <= 0) {
playSound('complete');
onComplete?.();
setIsRunning(false);
return 0;
}
return newTime;
});
}, 1000);
return () => clearInterval(interval);
}, [isRunning, timeLeft, onComplete, playSound]);
// Milestone detection
useEffect(() => {
const milestones = [75, 50, 25, 10, 5];
for (const milestone of milestones) {
if (percent <= milestone && lastMilestone > milestone) {
setLastMilestone(milestone);
playSound('milestone');
onMilestone?.(milestone);
break;
}
}
}, [percent, lastMilestone, onMilestone, playSound]);
// Reset timer
const reset = () => {
setTimeLeft(duration);
setIsRunning(false);
setLastMilestone(100);
};
// Toggle fullscreen
const toggleFullscreen = () => {
if (!document.fullscreenElement && containerRef.current) {
containerRef.current.requestFullscreen();
setIsFullscreen(true);
} else {
document.exitFullscreen();
setIsFullscreen(false);
}
};
// SVG calculations
const radius = (sizeConfig.ring - sizeConfig.stroke) / 2;
const circumference = radius * 2 * Math.PI;
const strokeDashoffset = circumference * (1 - progress);
return (
<div
ref={containerRef}
className={`
flex flex-col items-center justify-center gap-6 p-6
${isFullscreen ? 'fixed inset-0 bg-background z-50' : ''}
${settings.reducedAnimations ? '' : 'transition-all duration-300'}
`}
>
{taskTitle && (
<h3 className="text-lg font-medium text-center text-muted-foreground max-w-md">
{taskTitle}
</h3>
)}
{/* Visual Timer Ring */}
<div className="relative" style={{ width: sizeConfig.ring, height: sizeConfig.ring }}>
<svg
className="transform -rotate-90"
width={sizeConfig.ring}
height={sizeConfig.ring}
>
{/* Background circle */}
<circle
cx={sizeConfig.ring / 2}
cy={sizeConfig.ring / 2}
r={radius}
stroke={colors.bg}
strokeWidth={sizeConfig.stroke}
fill="none"
/>
{/* Progress circle */}
<motion.circle
cx={sizeConfig.ring / 2}
cy={sizeConfig.ring / 2}
r={radius}
stroke={getCurrentColor()}
strokeWidth={sizeConfig.stroke}
fill="none"
strokeLinecap="round"
strokeDasharray={circumference}
initial={{ strokeDashoffset: 0 }}
animate={{ strokeDashoffset }}
transition={{ duration: settings.reducedAnimations ? 0 : 0.5, ease: 'easeOut' }}
/>
</svg>
{/* Time display in center */}
{showNumbers && (
<div className="absolute inset-0 flex flex-col items-center justify-center">
<AnimatePresence mode="wait">
<motion.span
key={timeLeft}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: settings.reducedAnimations ? 0 : 0.2 }}
className={`font-mono font-bold ${sizeConfig.font}`}
style={{ color: getCurrentColor() }}
>
{formatTime(timeLeft)}
</motion.span>
</AnimatePresence>
<span className="text-sm text-muted-foreground mt-1">
{percent}% {t('adhd.timer.remaining')}
</span>
</div>
)}
</div>
{/* Controls */}
<div className="flex items-center gap-3">
<Button
variant="outline"
size="icon"
onClick={() => setIsRunning(!isRunning)}
className={settings.largerTargets ? 'h-12 w-12' : 'h-10 w-10'}
>
{isRunning ? (
<Pause className="h-5 w-5" />
) : (
<Play className="h-5 w-5" />
)}
</Button>
<Button
variant="outline"
size="icon"
onClick={reset}
className={settings.largerTargets ? 'h-12 w-12' : 'h-10 w-10'}
>
<RotateCcw className="h-5 w-5" />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => setSoundEnabled(!soundEnabled)}
className={settings.largerTargets ? 'h-12 w-12' : 'h-10 w-10'}
>
{soundEnabled ? (
<Volume2 className="h-5 w-5" />
) : (
<VolumeX className="h-5 w-5" />
)}
</Button>
<Button
variant="outline"
size="icon"
onClick={toggleFullscreen}
className={settings.largerTargets ? 'h-12 w-12' : 'h-10 w-10'}
>
{isFullscreen ? (
<Minimize2 className="h-5 w-5" />
) : (
<Maximize2 className="h-5 w-5" />
)}
</Button>
</div>
{/* Progress milestones */}
<div className="flex gap-2">
{[75, 50, 25, 0].map((milestone) => (
<div
key={milestone}
className={`
w-3 h-3 rounded-full transition-all
${percent <= milestone ? 'bg-primary' : 'bg-muted'}
`}
/>
))}
</div>
</div>
);
}
+310
View File
@@ -35,6 +35,7 @@
"weekList": "Woche",
"create": "Erstellen",
"kanban": "Kanban",
"adhd": "Fokus-Tools",
"achievements": "Erfolge",
"leaderboard": "Bestenliste",
"settings": "Einstellungen"
@@ -286,6 +287,18 @@
"2fa": "Zwei-Faktor-Authentifizierung",
"2faDesc": "Ihr Konto mit E-Mail-basierter 2FA sichern"
},
"schedule": {
"title": "Zeitplan-Einstellungen",
"description": "Definiere deine Verfügbarkeit für intelligente Planung.",
"start": "Startzeit",
"end": "Endzeit",
"days": "Aktive Tage",
"saved": "Zeitplan gespeichert",
"work": "Arbeitszeit",
"personal": "Persönliche Zeit",
"workDesc": "Aufgaben mit 'Arbeit'-Labels werden während dieser Zeiten geplant.",
"personalDesc": "Aufgaben mit 'Persönlich'-Labels werden während dieser Zeiten geplant. 'Neutrale' Aufgaben können beide nutzen."
},
"admin": {
"title": "Administration",
"description": "Systemweite Einstellungen und Benutzerverwaltung",
@@ -671,6 +684,17 @@
}
},
"analytics": {
"title": "Analysen",
"timeTracking": "Zeiterfassung",
"timeDistribution": "Zeitverteilung",
"totalTime": "Gesamtzeit",
"noData": "Keine Daten für diesen Zeitraum",
"period": {
"day": "Tag",
"week": "Woche",
"month": "Monat",
"year": "Jahr"
},
"mon": "Mo",
"tue": "Di",
"wed": "Mi",
@@ -905,5 +929,291 @@
"estimatedDuration": "Geschätzte Dauer",
"subtasks": "Teilaufgaben",
"timeEstimate": "Zeitschätzung"
},
"common": {
"optional": "Optional",
"saving": "Speichern...",
"creating": "Erstellen..."
},
"adhd": {
"mode": "Fokus-Tools",
"modeEnabled": "Fokus-Tools aktiviert",
"modeDisabled": "Fokus-Tools deaktiviert",
"modeDescription": "Optimiert die App für Fokus und Produktivität",
"toggleMode": "Fokus-Tools umschalten",
"active": "Aktiv",
"dashboard": {
"title": "Fokus-Dashboard",
"subtitle": "Werkzeuge, die zu deinem Gehirn passen",
"available": "verfügbar",
"completedToday": "Heute erledigt",
"coworking": "Zusammenarbeit",
"energyLevel": "Energie",
"focusMode": "Fokus-Modus",
"helpTitle": "Wie die Fokus-Tools dir helfen",
"helpIntro": "Diese Werkzeuge sind für dein Gehirn entwickelt - sie adressieren häufige Herausforderungen wie Zeitblindheit, Aufgabenlähmung und das Bedürfnis nach schnellen Erfolgen.",
"help": {
"quickWins": {
"title": "Schnelle Erfolge",
"why": "Das Starten von Aufgaben fühlt sich überwältigend an, und dein Gehirn sehnt sich nach schnellen Dopamin-Kicks.",
"how": "Zeigt nur Aufgaben unter 15 Minuten, sortiert nach Einfachheit. Kleine Erfolge bauen Momentum auf und geben deinem Gehirn die Belohnung, die es braucht.",
"usage": "Öffne Schnelle Erfolge, wenn du feststeckst. Wähle die erste Aufgabe und erledige sie. Der Dopamin-Boost hilft dir, mehr zu schaffen."
},
"singleTask": {
"title": "Einzelaufgaben-Fokus",
"why": "Eine lange Aufgabenliste zu sehen verursacht Überforderung und Lähmung.",
"how": "Versteckt alles außer EINER Aufgabe. Keine Ablenkungen, keine Listen-Angst - nur die Aufgabe vor dir.",
"usage": "Wähle eine Aufgabe und gehe in den Fokus-Modus. Arbeite nur an dieser Aufgabe, bis sie fertig ist oder du eine Pause brauchst."
},
"fiveMinute": {
"title": "5-Minuten-Starter",
"why": "Das Schwierigste ist der Anfang. Dein Gehirn wehrt sich gegen große Aufgaben.",
"how": "Verpflichte dich zu nur 5 Minuten. Dein Gehirn denkt: 'Ich kann alles 5 Minuten lang machen.' Einmal angefangen, machst du oft weiter.",
"usage": "Klicke '5 Min Start' bei einer Aufgabe. Arbeite 5 Minuten. Danach kannst du ohne schlechtes Gewissen aufhören ODER weitermachen."
},
"visualTimer": {
"title": "Visueller Timer",
"why": "Zeitblindheit - du kannst Zeit nicht natürlich wahrnehmen.",
"how": "Ein schrumpfender Kreis zeigt Zeit visuell an. Farben ändern sich (grün → gelb → rot) wenn die Zeit abläuft.",
"usage": "Nutze den Timer während Fokus-Sessions. Das visuelle Feedback hilft dir, Zeit zu spüren."
},
"breaks": {
"title": "Pausen-Erinnerungen",
"why": "Du vergisst Pausen zu machen, was zu Burnout oder Hyperfokus-Abstürzen führt.",
"how": "Sanfte Erinnerungen erscheinen nach deinem eingestellten Intervall mit Vorschlägen wie Dehnen oder Wasser trinken.",
"usage": "Wenn die Erinnerung erscheint, mach eine echte Pause. Auch 2 Minuten helfen. Du kannst bei Bedarf snoozen."
},
"hyperfocus": {
"title": "Hyperfokus-Schutz",
"why": "Du bleibst stundenlang an einer Aufgabe hängen und vernachlässigst alles andere.",
"how": "Warnt dich, wenn du zu lange an einer Aufgabe arbeitest. Zeigt andere wartende Aufgaben.",
"usage": "Bei einer Warnung überlege, ob du weitermachen oder wechseln solltest. Es ist okay weiterzumachen, aber mach es bewusst."
},
"energy": {
"title": "Energie-Check-ins",
"why": "Dich zu schweren Aufgaben zu zwingen, wenn du erschöpft bist, führt zum Scheitern.",
"how": "Logge täglich dein Energielevel. Die App schlägt Aufgaben vor, die zu deiner aktuellen Energie passen.",
"usage": "Checke jeden Morgen ein. Wenig Energie? Mach einfache Aufgaben. Viel Energie? Tackle Herausforderungen."
},
"bodyDoubling": {
"title": "Body Doubling",
"why": "Alleine zu arbeiten macht es schwer, am Ball zu bleiben.",
"how": "Virtuelle Co-Working-Sessions, bei denen du neben anderen arbeitest. Soziale Verantwortlichkeit hält dich fokussiert.",
"usage": "Tritt einer Session bei oder erstelle eine. Allein zu wissen, dass andere arbeiten, hilft dir dranzubleiben."
},
"taskBreakdown": {
"title": "KI-Aufgabenzerlegung",
"why": "Große Aufgaben fühlen sich unmöglich an zu starten.",
"how": "KI zerlegt überwältigende Aufgaben in kleine, konkrete Schritte mit Zeitschätzungen.",
"usage": "Klicke 'Zerlegen' bei einer großen Aufgabe. Überprüfe die Schritte und erstelle sie als Unteraufgaben."
}
},
"quickWins": "Schnelle Erfolge",
"streak": "Tagessträhne"
},
"timer": {
"remaining": "verbleibend"
},
"breakReminder": {
"title": "Zeit für eine Pause!",
"workingFor": "Du arbeitest seit {{minutes}} Minuten",
"message": "Dein Gehirn braucht regelmäßige Pausen. Wähle eine kurze Pause:",
"snooze5": "5 Min später",
"snooze15": "15 Min später",
"gentle": "Es ist okay, Pausen zu machen. Du machst das großartig!",
"types": {
"stretch": "Dehnen",
"water": "Wasser",
"walk": "Spazieren",
"eyes": "Augen",
"snack": "Snack",
"breathe": "Atmen"
}
},
"taskBreakdown": {
"title": "Aufgabe aufteilen",
"description": "Große Aufgaben können überwältigend sein. Lass uns sie in kleine Schritte zerlegen.",
"prompt": "Klicke unten, um diese Aufgabe in machbare Schritte zu zerlegen.",
"analyzing": "Analysiere...",
"breakItDown": "Aufgabe aufteilen",
"estimated": "geschätzt",
"totalTime": "Gesamtzeit",
"regenerate": "Neu generieren",
"createSubtasks": "{{count}} Schritte erstellen",
"error": "Aufgabe konnte nicht zerlegt werden. Bitte versuche es erneut."
},
"quickWins": {
"title": "Schnelle Erfolge",
"description": "Kleine Aufgaben für sofortige Dopamin-Boosts",
"tip": "Kleine Erfolge schaffen Momentum! Erledige diese schnellen Aufgaben für einen Dopamin-Boost.",
"noTasks": "Keine schnellen Aufgaben verfügbar"
},
"singleTask": {
"title": "Einzelaufgaben-Fokus",
"description": "Konzentriere dich auf eine Aufgabe ohne Ablenkungen",
"selectTask": "Wähle eine Aufgabe zum Fokussieren",
"choosePlaceholder": "Aufgabe auswählen...",
"task": "Aufgabe",
"complete": "erledigt",
"allDone": "Alles geschafft!",
"allDoneDesc": "Du hast alle Aufgaben erledigt. Nimm dir eine wohlverdiente Pause!",
"exit": "Ansicht verlassen",
"markComplete": "Als erledigt markieren",
"fiveMin": "5 Min starten",
"startTimer": "Timer starten",
"skip": "Überspringen"
},
"fiveMin": {
"title": "Die 5-Minuten-Regel",
"prompt": "Verpflichte dich nur zu 5 Minuten. Danach kannst du aufhören.",
"start": "5 Minuten starten",
"noObligation": "Keine Verpflichtung weiterzumachen!",
"remaining": "verbleibend",
"pause": "Pause",
"resume": "Fortsetzen",
"done": "Fertig!",
"encouragement": "Du machst das großartig! Jede Sekunde zählt.",
"congrats": "Super gemacht!",
"fiveMinDone": "Du hast 5 Minuten geschafft!",
"continueWorking": "Weitermachen",
"finished": "Aufgabe fertig!",
"takeBreak": "Pause machen",
"earned": "verdient"
},
"energy": {
"title": "Wie ist dein Energielevel?",
"description": "Hilf uns, dir passende Aufgaben vorzuschlagen.",
"checkIn": "Einchecken",
"checkInReminder": "Wie fühlst du dich heute?",
"checkInBenefit": "Tracke deine Energie für bessere Aufgabenvorschläge",
"notesLabel": "Notizen",
"notesPlaceholder": "Wie fühlst du dich heute?",
"submit": "Energie erfassen",
"levels": {
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch"
},
"suggestions": {
"low": {
"title": "Empfehlung für niedrige Energie",
"description": "Konzentriere dich auf einfache, administrative Aufgaben oder kurze Quick Wins."
},
"medium": {
"title": "Empfehlung für mittlere Energie",
"description": "Gut für Routineaufgaben und Kommunikation."
},
"high": {
"title": "Empfehlung für hohe Energie",
"description": "Perfekt für kreative oder herausfordernde Aufgaben!"
}
}
},
"hyperfocus": {
"title": "Hyperfokus-Warnung",
"message": "Du arbeitest seit {{minutes}} Minuten an dieser Aufgabe.",
"minutes": "Min",
"urgentTasks": "dringende Aufgaben",
"waitingTasks": "Wartende Aufgaben",
"takeBreak": "Pause machen",
"continue": "Weitermachen",
"gentle": "Es ist okay, Pausen zu machen oder zu wechseln."
},
"bodyDoubling": {
"title": "Body Doubling",
"description": "Arbeite gemeinsam mit anderen für mehr Fokus und Accountability.",
"tip": "Gemeinsames Arbeiten hilft beim Fokussieren. Tritt einer Session bei oder erstelle eine.",
"createSession": "Session erstellen",
"newSession": "Neue Session",
"newSessionDesc": "Erstelle eine Session, zu der andere beitreten können.",
"sessionTitle": "Titel",
"titlePlaceholder": "z.B. Fokus-Session Vormittag",
"sessionType": "Typ",
"types": {
"focus": "Fokus",
"brainstorm": "Brainstorm",
"admin": "Admin",
"creative": "Kreativ"
},
"startTime": "Startzeit",
"duration": "Dauer",
"create": "Session erstellen",
"activeSessions": "Aktive Sessions",
"upcomingSessions": "Geplante Sessions",
"noSessions": "Keine Sessions geplant. Erstelle die erste!",
"live": "Live",
"join": "Beitreten",
"leave": "Verlassen"
},
"settings": {
"title": "Fokus-Einstellungen",
"description": "Passe deine fokusfreundliche Erfahrung an",
"visualTitle": "Darstellung & Interaktion",
"visualDesc": "Passe das Interface für deine Bedürfnisse an.",
"reducedAnimations": "Reduzierte Animationen",
"reducedAnimationsDesc": "Weniger visuelle Bewegung für bessere Konzentration.",
"largerTargets": "Größere Buttons",
"largerTargetsDesc": "Größere Berührungsflächen für einfacheres Klicken.",
"singleTaskDefault": "Single-Task-Modus Standard",
"singleTaskDefaultDesc": "Zeige nur eine Aufgabe zur Zeit an.",
"timerTitle": "Timer & Fokus",
"timerDesc": "Einstellungen für Zeitmanagement.",
"visualTimer": "Visueller Timer",
"visualTimerDesc": "Zeige die Zeit als schrumpfenden Kreis.",
"hyperfocusProtection": "Hyperfokus-Schutz",
"hyperfocusProtectionDesc": "Warnungen bei zu langem Arbeiten an einer Aufgabe.",
"hyperfocusMaxMinutes": "Maximale Fokuszeit",
"breakTitle": "Pausen",
"breakDesc": "Erinnerungen für regelmäßige Pausen.",
"breakInterval": "Pausen-Intervall (Minuten)",
"motivationTitle": "Motivation",
"motivationDesc": "Positive Verstärkung und Belohnungen.",
"messagingLevel": "Ermutigungs-Level",
"messagingMinimal": "Minimal",
"messagingNormal": "Normal",
"messagingHigh": "Hoch",
"messagingLevelDesc": "Wie viel positive Ermutigung möchtest du?",
"quickWinThreshold": "Quick-Win Schwellwert",
"quickWinThresholdDesc": "Aufgaben unter dieser Dauer gelten als Quick Wins.",
"energyTitle": "Energie",
"energyDesc": "Tägliche Energie-Check-ins.",
"energyCheckIns": "Energie-Check-ins aktivieren",
"energyCheckInsDesc": "Täglich nach deinem Energielevel fragen.",
"reset": "Auf Standard zurücksetzen"
},
"encouragements": {
"taskComplete": [
"Großartig! Jeder Schritt zählt.",
"Du machst das super! Weiter so!",
"Ein Task weniger - du rockst das!",
"Siehst du? Du kannst das!",
"Fantastisch! Bleib dran!"
],
"returning": [
"Schön, dass du wieder da bist!",
"Jeder Neustart ist ein Gewinn.",
"Willkommen zurück! Lass uns loslegen."
],
"struggling": [
"Es ist okay, kleine Schritte zu machen.",
"Du musst nicht alles auf einmal schaffen.",
"Atme tief durch. Du schaffst das."
],
"streakBroken": [
"Streaks sind nur Zahlen. Du bist mehr als das.",
"Morgen ist ein neuer Tag!",
"Jeder Tag ist eine neue Chance."
],
"milestone": [
"Meilenstein erreicht! Unglaublich!",
"Du hast es geschafft! Feier diesen Moment!",
"Was für ein Erfolg!"
],
"quickWin": [
"Schneller Erfolg! Das Dopamin fließt!",
"Boom! Quick Win erledigt!",
"So schnell kann es gehen!"
]
}
}
}
+310
View File
@@ -35,6 +35,7 @@
"weekList": "Week",
"create": "Create",
"kanban": "Kanban",
"adhd": "Focus Tools",
"achievements": "Achievements",
"leaderboard": "Leaderboard",
"settings": "Settings"
@@ -286,6 +287,18 @@
"2fa": "Two-Factor Authentication",
"2faDesc": "Secure your account with email-based 2FA"
},
"schedule": {
"title": "Schedule Settings",
"description": "Define your availability for smart scheduling.",
"start": "Start Time",
"end": "End Time",
"days": "Active Days",
"saved": "Schedule saved",
"work": "Work Schedule",
"personal": "Personal Schedule",
"workDesc": "Tasks with 'Work' labels will be scheduled during these hours.",
"personalDesc": "Tasks with 'Personal' labels will be scheduled during these hours. 'Neutral' tasks can use either."
},
"admin": {
"title": "Admin Settings",
"description": "System administration and configuration",
@@ -783,6 +796,17 @@
}
},
"analytics": {
"title": "Analytics",
"timeTracking": "Time Tracking",
"timeDistribution": "Time Distribution",
"totalTime": "Total Time",
"noData": "No data recorded for this period",
"period": {
"day": "Day",
"week": "Week",
"month": "Month",
"year": "Year"
},
"mon": "Mon",
"tue": "Tue",
"wed": "Wed",
@@ -892,5 +916,291 @@
"estimatedDuration": "Estimated Duration",
"subtasks": "Subtasks",
"timeEstimate": "Time Estimate"
},
"common": {
"optional": "Optional",
"saving": "Saving...",
"creating": "Creating..."
},
"adhd": {
"mode": "Focus Tools",
"modeEnabled": "Focus Tools enabled",
"modeDisabled": "Focus Tools disabled",
"modeDescription": "Optimizes the app for focus and productivity",
"toggleMode": "Toggle Focus Tools",
"active": "Active",
"dashboard": {
"title": "Focus Dashboard",
"subtitle": "Tools designed for how your brain works",
"available": "available",
"completedToday": "Done today",
"coworking": "Co-working",
"energyLevel": "Energy",
"focusMode": "Focus mode",
"helpTitle": "How Focus Tools Help You",
"helpIntro": "These tools are designed around how your brain works - addressing common challenges like time blindness, task paralysis, and the need for quick wins.",
"help": {
"quickWins": {
"title": "Quick Wins",
"why": "Starting tasks feels overwhelming, and your brain craves quick dopamine hits.",
"how": "Shows only tasks under 15 minutes, sorted by easiest to complete. Completing small tasks builds momentum and gives your brain the reward it needs.",
"usage": "Open Quick Wins when you feel stuck. Pick the first task and complete it. The dopamine boost will help you tackle more."
},
"singleTask": {
"title": "Single Task Focus",
"why": "Seeing a long task list causes overwhelm and paralysis.",
"how": "Hides everything except ONE task. No distractions, no list anxiety - just the task in front of you.",
"usage": "Select a task and enter focus mode. Work only on that task until done or you need a break."
},
"fiveMinute": {
"title": "5-Minute Starter",
"why": "The hardest part is beginning. Your brain resists starting big tasks.",
"how": "Commit to just 5 minutes. Your brain thinks 'I can do anything for 5 minutes.' Once started, you often keep going.",
"usage": "Click '5 Min Start' on any task. Work for 5 minutes. After that, you can stop guilt-free OR continue."
},
"visualTimer": {
"title": "Visual Timer",
"why": "Time blindness - you can't feel time passing naturally.",
"how": "A shrinking circle shows time visually. Colors change (green → yellow → red) as time runs out.",
"usage": "Use the timer during focus sessions. The visual feedback helps you feel time passing."
},
"breaks": {
"title": "Break Reminders",
"why": "You forget to take breaks, leading to burnout or hyperfocus crashes.",
"how": "Gentle reminders appear after your set interval suggesting quick breaks like stretching or water.",
"usage": "When the reminder appears, take a real break. Even 2 minutes helps. You can snooze if needed."
},
"hyperfocus": {
"title": "Hyperfocus Protection",
"why": "You get stuck on one task for hours, neglecting everything else.",
"how": "Warns you when you've been on a single task too long. Shows other waiting tasks.",
"usage": "When warned, assess if you should continue or switch. It's okay to keep going, but make it a conscious choice."
},
"energy": {
"title": "Energy Check-ins",
"why": "Forcing yourself to do hard tasks when exhausted leads to failure.",
"how": "Log your energy level daily. The app suggests tasks that match your current energy.",
"usage": "Check in each morning. Low energy? Do simple tasks. High energy? Tackle challenges."
},
"bodyDoubling": {
"title": "Body Doubling",
"why": "Working alone makes it hard to stay accountable.",
"how": "Virtual co-working sessions where you work alongside others. Social accountability keeps you focused.",
"usage": "Join or create a session. Just knowing others are working helps you stay on track."
},
"taskBreakdown": {
"title": "AI Task Breakdown",
"why": "Big tasks feel impossible to start.",
"how": "AI breaks down overwhelming tasks into small, concrete steps with time estimates.",
"usage": "Click 'Break It Down' on any large task. Review the steps and create them as subtasks."
}
},
"quickWins": "Quick wins",
"streak": "Day streak"
},
"timer": {
"remaining": "remaining"
},
"breakReminder": {
"title": "Time for a break!",
"workingFor": "You've been working for {{minutes}} minutes",
"message": "Your brain needs regular breaks. Choose a quick break:",
"snooze5": "5 min later",
"snooze15": "15 min later",
"gentle": "It's okay to take breaks. You're doing great!",
"types": {
"stretch": "Stretch",
"water": "Water",
"walk": "Walk",
"eyes": "Eyes",
"snack": "Snack",
"breathe": "Breathe"
}
},
"taskBreakdown": {
"title": "Break Down Task",
"description": "Large tasks can be overwhelming. Let's break them into smaller steps.",
"prompt": "Click below to break this task into manageable steps.",
"analyzing": "Analyzing...",
"breakItDown": "Break It Down",
"estimated": "estimated",
"totalTime": "Total time",
"regenerate": "Regenerate",
"createSubtasks": "Create {{count}} steps",
"error": "Failed to break down task. Please try again."
},
"quickWins": {
"title": "Quick Wins",
"description": "Small tasks for instant dopamine boosts",
"tip": "Small wins build momentum! Complete these quick tasks to boost your dopamine and build confidence.",
"noTasks": "No quick tasks available"
},
"singleTask": {
"title": "Single Task Focus",
"description": "Focus on one task at a time without distractions",
"selectTask": "Select a task to focus on",
"choosePlaceholder": "Choose a task...",
"task": "Task",
"complete": "complete",
"allDone": "All Done!",
"allDoneDesc": "You've completed all tasks. Take a well-deserved break!",
"exit": "Exit view",
"markComplete": "Mark Complete",
"fiveMin": "Start 5 Min",
"startTimer": "Start Timer",
"skip": "Skip"
},
"fiveMin": {
"title": "The 5-Minute Rule",
"prompt": "Commit to just 5 minutes. You can stop after that.",
"start": "Start 5 Minutes",
"noObligation": "No obligation to continue!",
"remaining": "remaining",
"pause": "Pause",
"resume": "Resume",
"done": "Done!",
"encouragement": "You're doing great! Every second counts.",
"congrats": "Well done!",
"fiveMinDone": "You made it through 5 minutes!",
"continueWorking": "Keep Going",
"finished": "Task finished!",
"takeBreak": "Take a break",
"earned": "earned"
},
"energy": {
"title": "How's your energy level?",
"description": "Help us suggest the right tasks for you.",
"checkIn": "Check In",
"checkInReminder": "How are you feeling today?",
"checkInBenefit": "Track your energy to get better task suggestions",
"notesLabel": "Notes",
"notesPlaceholder": "How are you feeling today?",
"submit": "Log Energy",
"levels": {
"low": "Low",
"medium": "Medium",
"high": "High"
},
"suggestions": {
"low": {
"title": "Low energy recommendation",
"description": "Focus on simple, administrative tasks or quick wins."
},
"medium": {
"title": "Medium energy recommendation",
"description": "Good for routine tasks and communication."
},
"high": {
"title": "High energy recommendation",
"description": "Perfect for creative or challenging tasks!"
}
}
},
"hyperfocus": {
"title": "Hyperfocus Warning",
"message": "You've been working on this task for {{minutes}} minutes.",
"minutes": "min",
"urgentTasks": "urgent tasks",
"waitingTasks": "Waiting tasks",
"takeBreak": "Take a break",
"continue": "Continue",
"gentle": "It's okay to take breaks or switch tasks."
},
"bodyDoubling": {
"title": "Body Doubling",
"description": "Work alongside others for more focus and accountability.",
"tip": "Working alongside others helps maintain focus. Join or create a session to stay accountable.",
"createSession": "Create Session",
"newSession": "New Session",
"newSessionDesc": "Create a session others can join.",
"sessionTitle": "Title",
"titlePlaceholder": "e.g. Morning Focus Session",
"sessionType": "Type",
"types": {
"focus": "Focus",
"brainstorm": "Brainstorm",
"admin": "Admin",
"creative": "Creative"
},
"startTime": "Start Time",
"duration": "Duration",
"create": "Create Session",
"activeSessions": "Active Sessions",
"upcomingSessions": "Upcoming Sessions",
"noSessions": "No sessions scheduled. Create the first one!",
"live": "Live",
"join": "Join",
"leave": "Leave"
},
"settings": {
"title": "Focus Settings",
"description": "Customize your focus-friendly experience",
"visualTitle": "Display & Interaction",
"visualDesc": "Customize the interface for your needs.",
"reducedAnimations": "Reduced Animations",
"reducedAnimationsDesc": "Less visual motion for better focus.",
"largerTargets": "Larger Buttons",
"largerTargetsDesc": "Bigger touch targets for easier clicking.",
"singleTaskDefault": "Single Task Mode Default",
"singleTaskDefaultDesc": "Show only one task at a time.",
"timerTitle": "Timer & Focus",
"timerDesc": "Settings for time management.",
"visualTimer": "Visual Timer",
"visualTimerDesc": "Show time as a shrinking circle.",
"hyperfocusProtection": "Hyperfocus Protection",
"hyperfocusProtectionDesc": "Warnings when working too long on one task.",
"hyperfocusMaxMinutes": "Maximum Focus Time",
"breakTitle": "Breaks",
"breakDesc": "Reminders for regular breaks.",
"breakInterval": "Break Interval (minutes)",
"motivationTitle": "Motivation",
"motivationDesc": "Positive reinforcement and rewards.",
"messagingLevel": "Encouragement Level",
"messagingMinimal": "Minimal",
"messagingNormal": "Normal",
"messagingHigh": "High",
"messagingLevelDesc": "How much positive encouragement do you want?",
"quickWinThreshold": "Quick Win Threshold",
"quickWinThresholdDesc": "Tasks under this duration are considered Quick Wins.",
"energyTitle": "Energy",
"energyDesc": "Daily energy check-ins.",
"energyCheckIns": "Enable Energy Check-ins",
"energyCheckInsDesc": "Ask about your energy level daily.",
"reset": "Reset to Defaults"
},
"encouragements": {
"taskComplete": [
"Amazing! Every step counts.",
"You're doing great! Keep it up!",
"One less task - you rock!",
"See? You can do it!",
"Fantastic! Stay on track!"
],
"returning": [
"Good to see you back!",
"Every fresh start is a win.",
"Welcome back! Let's get started."
],
"struggling": [
"It's okay to take small steps.",
"You don't have to do everything at once.",
"Take a deep breath. You've got this."
],
"streakBroken": [
"Streaks are just numbers. You're more than that.",
"Tomorrow is a new day!",
"Every day is a fresh chance."
],
"milestone": [
"Milestone reached! Incredible!",
"You made it! Celebrate this moment!",
"What an achievement!"
],
"quickWin": [
"Quick win! The dopamine is flowing!",
"Boom! Quick win complete!",
"That was fast!"
]
}
}
}
+249
View File
@@ -327,4 +327,253 @@
.border.hover-elevate:not(.no-hover-interaction-elevate)::after {
inset: -1px;
}
}
/* ============================================
ADHD MODE STYLES
Calmer colors, reduced cognitive load,
larger touch targets, and time-awareness
============================================ */
/* ADHD Mode Variables */
:root {
--adhd-timer-green: 142 76% 36%;
--adhd-timer-yellow: 45 93% 47%;
--adhd-timer-red: 0 84% 60%;
--adhd-calm-primary: 210 40% 52%;
--adhd-calm-accent: 199 89% 48%;
--adhd-success-green: 142 71% 45%;
}
.dark {
--adhd-timer-green: 142 76% 46%;
--adhd-timer-yellow: 45 93% 57%;
--adhd-timer-red: 0 84% 65%;
--adhd-calm-primary: 210 50% 62%;
--adhd-calm-accent: 199 89% 58%;
--adhd-success-green: 142 71% 55%;
}
/* ADHD Mode - Global adjustments when enabled */
.adhd-mode {
/* Calmer color palette */
--primary: var(--adhd-calm-primary);
--ring: var(--adhd-calm-accent);
/* Larger border radius for softer appearance */
--radius: 1.25rem;
}
.adhd-mode .card,
.adhd-mode .dialog-content,
.adhd-mode .popover-content {
border-radius: var(--radius);
}
/* Reduced Motion - Disable animations for users who need focus */
.reduced-motion,
.reduced-motion * {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
/* 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"]) {
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"] {
min-height: 44px;
min-width: 44px;
}
/* Form inputs and selects */
.larger-targets input,
.larger-targets select,
.larger-targets textarea {
min-height: 44px;
}
/* Links only need height adjustment when they look like buttons */
.larger-targets a[role="button"] {
min-height: 44px;
}
.larger-targets .sidebar-menu-button {
min-height: 52px;
}
/* Visual Timer Color Classes */
.timer-green {
color: hsl(var(--adhd-timer-green));
}
.timer-yellow {
color: hsl(var(--adhd-timer-yellow));
}
.timer-red {
color: hsl(var(--adhd-timer-red));
}
.timer-stroke-green {
stroke: hsl(var(--adhd-timer-green));
}
.timer-stroke-yellow {
stroke: hsl(var(--adhd-timer-yellow));
}
.timer-stroke-red {
stroke: hsl(var(--adhd-timer-red));
}
/* Break Reminder Pulse Animation */
@keyframes break-reminder-pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.8;
transform: scale(1.02);
}
}
.break-reminder-pulse {
animation: break-reminder-pulse 2s ease-in-out infinite;
}
/* Encouragement Toast Animation */
@keyframes encouragement-bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
.encouragement-bounce {
animation: encouragement-bounce 0.5s ease-out;
}
/* Quick Win Card Highlight */
@keyframes quick-win-glow {
0%, 100% {
box-shadow: 0 0 5px 0 hsl(var(--adhd-success-green) / 0.3);
}
50% {
box-shadow: 0 0 15px 2px hsl(var(--adhd-success-green) / 0.5);
}
}
.quick-win-highlight {
animation: quick-win-glow 2s ease-in-out infinite;
}
/* XP Micro-Reward Animation */
@keyframes xp-float {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-30px) scale(1.2);
}
}
.xp-float {
animation: xp-float 1s ease-out forwards;
}
/* Focus Mode Overlay */
.single-task-overlay {
backdrop-filter: blur(4px);
background: hsl(var(--background) / 0.95);
}
/* Energy Level Indicators */
.energy-low {
border-left: 4px solid hsl(var(--adhd-timer-red));
}
.energy-medium {
border-left: 4px solid hsl(var(--adhd-timer-yellow));
}
.energy-high {
border-left: 4px solid hsl(var(--adhd-timer-green));
}
/* Hyperfocus Warning Styles */
@keyframes hyperfocus-warning {
0%, 100% {
border-color: hsl(var(--adhd-timer-yellow));
}
50% {
border-color: hsl(var(--adhd-timer-red));
}
}
.hyperfocus-warning {
animation: hyperfocus-warning 1.5s ease-in-out infinite;
border-width: 2px;
border-style: solid;
}
/* Body Doubling Session Active Indicator */
@keyframes session-active-pulse {
0%, 100% {
box-shadow: 0 0 0 0 hsl(var(--adhd-calm-accent) / 0.4);
}
50% {
box-shadow: 0 0 0 8px hsl(var(--adhd-calm-accent) / 0);
}
}
.session-active {
animation: session-active-pulse 2s ease-out infinite;
}
/* ADHD Settings Panel Styles */
.adhd-settings-section {
@apply p-4 rounded-xl bg-muted/50 border border-border;
}
.adhd-settings-section h3 {
@apply text-lg font-semibold mb-4 flex items-center gap-2;
}
/* Task Breakdown Subtask Styles */
.subtask-item {
@apply p-3 rounded-lg bg-background border border-border hover:border-primary/50 transition-colors cursor-pointer;
}
.subtask-item.completed {
@apply bg-muted/50 opacity-70;
}
/* Five Minute Starter Progress Ring */
.five-min-progress {
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
/* Calm Background for Focus Sessions */
.calm-gradient {
background: linear-gradient(
135deg,
hsl(var(--background)) 0%,
hsl(var(--adhd-calm-primary) / 0.05) 50%,
hsl(var(--adhd-calm-accent) / 0.05) 100%
);
}
+392
View File
@@ -0,0 +1,392 @@
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { useADHDMode, ADHDSettingsPanel, EnergyCheckIn } from '@/components/adhd';
import { User, Task } from '@shared/schema';
import { Brain, Zap, Focus, Users, Battery, TrendingUp, ArrowLeft, HelpCircle, Clock, Coffee, Sparkles, Timer, Split } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { useLocation } from 'wouter';
import { useState } from 'react';
export default function ADHDDashboardPage() {
const { t } = useTranslation();
const [, setLocation] = useLocation();
const { isEnabled, settings } = useADHDMode();
const [showEnergyCheckIn, setShowEnergyCheckIn] = useState(false);
const { data: user } = useQuery<User>({
queryKey: ['/api/user'],
});
const { data: tasks = [] } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
const { data: breakStats } = useQuery({
queryKey: ['/api/user/break-stats'],
enabled: isEnabled,
});
const { data: energyHistory } = useQuery({
queryKey: ['/api/energy/history'],
enabled: isEnabled,
});
const completedToday = tasks.filter(t => {
if (t.status !== 'done' || !t.updatedAt) return false;
const today = new Date();
const updated = new Date(t.updatedAt);
return updated.toDateString() === today.toDateString();
}).length;
const quickWins = tasks.filter(t =>
t.status !== 'done' &&
t.estimatedDuration &&
t.estimatedDuration <= 15
).length;
const features = [
{
icon: Zap,
title: t('adhd.quickWins.title'),
description: t('adhd.quickWins.description'),
path: '/adhd/quick-wins',
color: 'from-yellow-400 to-orange-500',
stat: `${quickWins} ${t('adhd.dashboard.available', 'available')}`,
},
{
icon: Focus,
title: t('adhd.singleTask.title'),
description: t('adhd.singleTask.description'),
path: '/adhd/single-task',
color: 'from-blue-400 to-indigo-500',
stat: t('adhd.dashboard.focusMode', 'Focus mode'),
},
{
icon: Users,
title: t('adhd.bodyDoubling.title'),
description: t('adhd.bodyDoubling.description'),
path: '/adhd/body-doubling',
color: 'from-green-400 to-teal-500',
stat: t('adhd.dashboard.coworking', 'Co-working'),
},
];
return (
<div className="max-w-6xl mx-auto space-y-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/')}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-purple-400 to-pink-500 text-white">
<Brain className="h-6 w-6" />
</div>
<div>
<h1 className="text-2xl font-bold">{t('adhd.dashboard.title', 'ADHD Dashboard')}</h1>
<p className="text-muted-foreground">{t('adhd.dashboard.subtitle', 'Tools designed for how your brain works')}</p>
</div>
</div>
</div>
{/* Stats Row */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-900/30">
<TrendingUp className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="text-2xl font-bold">{completedToday}</p>
<p className="text-xs text-muted-foreground">{t('adhd.dashboard.completedToday', 'Done today')}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900/30">
<Battery className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="text-2xl font-bold capitalize">{user?.currentEnergyLevel || '-'}</p>
<p className="text-xs text-muted-foreground">{t('adhd.dashboard.energyLevel', 'Energy')}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-yellow-100 dark:bg-yellow-900/30">
<Zap className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
</div>
<div>
<p className="text-2xl font-bold">{quickWins}</p>
<p className="text-xs text-muted-foreground">{t('adhd.dashboard.quickWins', 'Quick wins')}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-purple-100 dark:bg-purple-900/30">
<Brain className="h-5 w-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<p className="text-2xl font-bold">{user?.currentStreak || 0}</p>
<p className="text-xs text-muted-foreground">{t('adhd.dashboard.streak', 'Day streak')}</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Energy Check-in Button */}
{isEnabled && settings.energyCheckInsEnabled && !user?.todayEnergyCheckedIn && (
<Card className="border-2 border-dashed border-primary/50 bg-primary/5">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="font-medium">{t('adhd.energy.checkInReminder', 'How are you feeling today?')}</p>
<p className="text-sm text-muted-foreground">{t('adhd.energy.checkInBenefit', 'Track your energy to get better task suggestions')}</p>
</div>
<Button onClick={() => setShowEnergyCheckIn(true)}>
{t('adhd.energy.checkIn', 'Check In')}
</Button>
</CardContent>
</Card>
)}
{/* Feature Cards */}
<div className="grid md:grid-cols-3 gap-4">
{features.map((feature) => (
<Card
key={feature.path}
className="cursor-pointer hover:shadow-lg transition-all hover:-translate-y-1"
onClick={() => setLocation(feature.path)}
>
<CardHeader>
<div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center mb-2`}>
<feature.icon className="h-6 w-6 text-white" />
</div>
<CardTitle className="text-lg">{feature.title}</CardTitle>
<CardDescription>{feature.description}</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm font-medium text-primary">{feature.stat}</p>
</CardContent>
</Card>
))}
</div>
{/* Settings Panel */}
<Card>
<CardHeader>
<CardTitle>{t('adhd.settings.title', 'ADHD Settings')}</CardTitle>
<CardDescription>{t('adhd.settings.description', 'Customize your ADHD-friendly experience')}</CardDescription>
</CardHeader>
<CardContent>
<ADHDSettingsPanel />
</CardContent>
</Card>
{/* Collapsible Help Section */}
<Card>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="help" className="border-none">
<AccordionTrigger className="px-6 py-4 hover:no-underline">
<div className="flex items-center gap-3">
<HelpCircle className="h-5 w-5 text-primary" />
<span className="font-semibold">{t('adhd.dashboard.helpTitle', 'How Focus Tools Help You')}</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<p className="text-muted-foreground mb-6">
{t('adhd.dashboard.helpIntro', 'These tools are designed around how your brain works, not against it. Each feature addresses a specific challenge and provides practical strategies to help you get things done.')}
</p>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{/* Quick Wins */}
<div className="p-4 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800">
<div className="flex items-center gap-2 mb-2">
<Zap className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />
<h4 className="font-medium">{t('adhd.dashboard.help.quickWins.title', 'Quick Wins')}</h4>
</div>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.why', 'Why')}:</strong> {t('adhd.dashboard.help.quickWins.why', 'Starting tasks feels overwhelming when everything seems big.')}
</p>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.how', 'How')}:</strong> {t('adhd.dashboard.help.quickWins.how', 'Shows only tasks under 15 minutes. Small wins build momentum and dopamine.')}
</p>
<p className="text-xs text-muted-foreground">
<strong>{t('adhd.dashboard.help.usage', 'Use it')}:</strong> {t('adhd.dashboard.help.quickWins.usage', 'Open Quick Wins when you feel stuck or need to build momentum.')}
</p>
</div>
{/* Single Task Focus */}
<div className="p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
<div className="flex items-center gap-2 mb-2">
<Focus className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<h4 className="font-medium">{t('adhd.dashboard.help.singleTask.title', 'Single Task Focus')}</h4>
</div>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.why', 'Why')}:</strong> {t('adhd.dashboard.help.singleTask.why', 'Task lists are overwhelming. Seeing everything makes it hard to start anything.')}
</p>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.how', 'How')}:</strong> {t('adhd.dashboard.help.singleTask.how', 'Shows only ONE task at a time. No distractions, no choices, just focus.')}
</p>
<p className="text-xs text-muted-foreground">
<strong>{t('adhd.dashboard.help.usage', 'Use it')}:</strong> {t('adhd.dashboard.help.singleTask.usage', 'When you have many tasks and feel paralyzed by choice.')}
</p>
</div>
{/* 5-Minute Starter */}
<div className="p-4 rounded-lg bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800">
<div className="flex items-center gap-2 mb-2">
<Clock className="h-4 w-4 text-orange-600 dark:text-orange-400" />
<h4 className="font-medium">{t('adhd.dashboard.help.fiveMinute.title', '5-Minute Starter')}</h4>
</div>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.why', 'Why')}:</strong> {t('adhd.dashboard.help.fiveMinute.why', 'Getting started is the hardest part. Commitment feels impossible.')}
</p>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.how', 'How')}:</strong> {t('adhd.dashboard.help.fiveMinute.how', 'Commit to just 5 minutes. Often, starting leads to continuing naturally.')}
</p>
<p className="text-xs text-muted-foreground">
<strong>{t('adhd.dashboard.help.usage', 'Use it')}:</strong> {t('adhd.dashboard.help.fiveMinute.usage', 'For tasks you keep avoiding. Just 5 minutes, then decide if you continue.')}
</p>
</div>
{/* Visual Timer */}
<div className="p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
<div className="flex items-center gap-2 mb-2">
<Timer className="h-4 w-4 text-red-600 dark:text-red-400" />
<h4 className="font-medium">{t('adhd.dashboard.help.visualTimer.title', 'Visual Timer')}</h4>
</div>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.why', 'Why')}:</strong> {t('adhd.dashboard.help.visualTimer.why', 'Time blindness makes it hard to feel time passing.')}
</p>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.how', 'How')}:</strong> {t('adhd.dashboard.help.visualTimer.how', 'A visual countdown shows time disappearing, making it tangible.')}
</p>
<p className="text-xs text-muted-foreground">
<strong>{t('adhd.dashboard.help.usage', 'Use it')}:</strong> {t('adhd.dashboard.help.visualTimer.usage', 'Set timers for work sessions. The visual helps maintain awareness.')}
</p>
</div>
{/* Break Reminders */}
<div className="p-4 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
<div className="flex items-center gap-2 mb-2">
<Coffee className="h-4 w-4 text-green-600 dark:text-green-400" />
<h4 className="font-medium">{t('adhd.dashboard.help.breaks.title', 'Break Reminders')}</h4>
</div>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.why', 'Why')}:</strong> {t('adhd.dashboard.help.breaks.why', 'Hyperfocus makes you forget basic needs like rest, food, water.')}
</p>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.how', 'How')}:</strong> {t('adhd.dashboard.help.breaks.how', 'Gentle reminders interrupt hyperfocus before burnout.')}
</p>
<p className="text-xs text-muted-foreground">
<strong>{t('adhd.dashboard.help.usage', 'Use it')}:</strong> {t('adhd.dashboard.help.breaks.usage', 'Enable in settings. Take the breaks even when you feel productive.')}
</p>
</div>
{/* Energy Tracking */}
<div className="p-4 rounded-lg bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800">
<div className="flex items-center gap-2 mb-2">
<Battery className="h-4 w-4 text-purple-600 dark:text-purple-400" />
<h4 className="font-medium">{t('adhd.dashboard.help.energy.title', 'Energy Tracking')}</h4>
</div>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.why', 'Why')}:</strong> {t('adhd.dashboard.help.energy.why', 'Energy fluctuates unpredictably. Fighting low energy wastes time.')}
</p>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.how', 'How')}:</strong> {t('adhd.dashboard.help.energy.how', 'Track your energy to match tasks to your current state.')}
</p>
<p className="text-xs text-muted-foreground">
<strong>{t('adhd.dashboard.help.usage', 'Use it')}:</strong> {t('adhd.dashboard.help.energy.usage', 'Check in daily. Do hard tasks when high, easy tasks when low.')}
</p>
</div>
{/* Body Doubling */}
<div className="p-4 rounded-lg bg-teal-50 dark:bg-teal-900/20 border border-teal-200 dark:border-teal-800">
<div className="flex items-center gap-2 mb-2">
<Users className="h-4 w-4 text-teal-600 dark:text-teal-400" />
<h4 className="font-medium">{t('adhd.dashboard.help.bodyDoubling.title', 'Body Doubling')}</h4>
</div>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.why', 'Why')}:</strong> {t('adhd.dashboard.help.bodyDoubling.why', 'Working alone feels impossible. Presence of others helps focus.')}
</p>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.how', 'How')}:</strong> {t('adhd.dashboard.help.bodyDoubling.how', 'Virtual co-working sessions simulate having someone nearby.')}
</p>
<p className="text-xs text-muted-foreground">
<strong>{t('adhd.dashboard.help.usage', 'Use it')}:</strong> {t('adhd.dashboard.help.bodyDoubling.usage', 'Join a session when working from home feels isolating.')}
</p>
</div>
{/* Task Breakdown */}
<div className="p-4 rounded-lg bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-800">
<div className="flex items-center gap-2 mb-2">
<Split className="h-4 w-4 text-indigo-600 dark:text-indigo-400" />
<h4 className="font-medium">{t('adhd.dashboard.help.taskBreakdown.title', 'AI Task Breakdown')}</h4>
</div>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.why', 'Why')}:</strong> {t('adhd.dashboard.help.taskBreakdown.why', 'Big tasks feel impossible to start when you cannot see the steps.')}
</p>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.how', 'How')}:</strong> {t('adhd.dashboard.help.taskBreakdown.how', 'AI breaks large tasks into small, actionable subtasks automatically.')}
</p>
<p className="text-xs text-muted-foreground">
<strong>{t('adhd.dashboard.help.usage', 'Use it')}:</strong> {t('adhd.dashboard.help.taskBreakdown.usage', 'Click breakdown on any overwhelming task to get clear next steps.')}
</p>
</div>
{/* Hyperfocus Protection */}
<div className="p-4 rounded-lg bg-pink-50 dark:bg-pink-900/20 border border-pink-200 dark:border-pink-800">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="h-4 w-4 text-pink-600 dark:text-pink-400" />
<h4 className="font-medium">{t('adhd.dashboard.help.hyperfocus.title', 'Hyperfocus Protection')}</h4>
</div>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.why', 'Why')}:</strong> {t('adhd.dashboard.help.hyperfocus.why', 'Hyperfocus can make you lose hours on one thing while neglecting others.')}
</p>
<p className="text-xs text-muted-foreground mb-2">
<strong>{t('adhd.dashboard.help.how', 'How')}:</strong> {t('adhd.dashboard.help.hyperfocus.how', 'Alerts when you spend too long on a single task.')}
</p>
<p className="text-xs text-muted-foreground">
<strong>{t('adhd.dashboard.help.usage', 'Use it')}:</strong> {t('adhd.dashboard.help.hyperfocus.usage', 'Enable in settings. When alerted, save your progress and decide consciously.')}
</p>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</Card>
{/* Energy Check-in Modal */}
{showEnergyCheckIn && (
<EnergyCheckIn
onClose={() => setShowEnergyCheckIn(false)}
onSubmit={() => setShowEnergyCheckIn(false)}
/>
)}
</div>
);
}
+55
View File
@@ -0,0 +1,55 @@
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { BodyDoublingLobby } from '@/components/adhd';
import { User } from '@shared/schema';
import { Users, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useLocation } from 'wouter';
export default function BodyDoublingPage() {
const { t } = useTranslation();
const [, setLocation] = useLocation();
const { data: user } = useQuery<User>({
queryKey: ['/api/user'],
});
if (!user) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<p className="text-muted-foreground">{t('common.loading', 'Loading...')}</p>
</div>
);
}
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/')}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-green-400 to-teal-500 text-white">
<Users className="h-6 w-6" />
</div>
<div>
<h1 className="text-2xl font-bold">{t('adhd.bodyDoubling.title')}</h1>
<p className="text-muted-foreground">{t('adhd.bodyDoubling.description')}</p>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-teal-50 dark:from-green-950/20 dark:to-teal-950/20 rounded-2xl p-6 border border-green-200 dark:border-green-800">
<p className="text-sm text-muted-foreground mb-4">
{t('adhd.bodyDoubling.tip', 'Working alongside others helps maintain focus. Join or create a session to stay accountable.')}
</p>
<BodyDoublingLobby currentUserId={user.id} />
</div>
</div>
);
}
+64
View File
@@ -0,0 +1,64 @@
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { QuickWinList } from '@/components/adhd';
import { Task } from '@shared/schema';
import { Zap, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useLocation } from 'wouter';
export default function QuickWinsPage() {
const { t } = useTranslation();
const [, setLocation] = useLocation();
const { data: tasks = [] } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
const handleTaskSelect = (taskId: string) => {
setLocation(`/tasks?selected=${taskId}`);
};
const handleTaskComplete = async (taskId: string) => {
await fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'done' }),
});
};
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/')}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-yellow-400 to-orange-500 text-white">
<Zap className="h-6 w-6" />
</div>
<div>
<h1 className="text-2xl font-bold">{t('adhd.quickWins.title')}</h1>
<p className="text-muted-foreground">{t('adhd.quickWins.description')}</p>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-yellow-50 to-orange-50 dark:from-yellow-950/20 dark:to-orange-950/20 rounded-2xl p-6 border border-yellow-200 dark:border-yellow-800">
<p className="text-sm text-muted-foreground mb-4">
{t('adhd.quickWins.tip', 'Small wins build momentum! Complete these quick tasks to boost your dopamine and build confidence.')}
</p>
<QuickWinList
tasks={tasks.filter(t => t.status !== 'done')}
maxDuration={15}
onTaskSelect={handleTaskSelect}
onTaskComplete={handleTaskComplete}
/>
</div>
</div>
);
}
+121
View File
@@ -0,0 +1,121 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { SingleTaskView, FiveMinuteStarter } from '@/components/adhd';
import { Task } from '@shared/schema';
import { Focus, ArrowLeft, Timer } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useLocation } from 'wouter';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SingleTaskPage() {
const { t } = useTranslation();
const [, setLocation] = useLocation();
const queryClient = useQueryClient();
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const [showFiveMinStarter, setShowFiveMinStarter] = useState(false);
const { data: tasks = [] } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
const activeTasks = tasks.filter(t => t.status !== 'done');
const selectedTask = selectedTaskId ? tasks.find(t => t.id === selectedTaskId) : null;
const handleTaskComplete = async (taskId: string) => {
await fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'done' }),
});
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
setSelectedTaskId(null);
};
const handleTaskUpdate = async (taskId: string, updates: Partial<Task>) => {
await fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
};
const handleFiveMinComplete = () => {
setShowFiveMinStarter(false);
// Continue with the task
};
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => setLocation('/')}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-blue-400 to-indigo-500 text-white">
<Focus className="h-6 w-6" />
</div>
<div>
<h1 className="text-2xl font-bold">{t('adhd.singleTask.title')}</h1>
<p className="text-muted-foreground">{t('adhd.singleTask.description')}</p>
</div>
</div>
</div>
{!selectedTask ? (
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 rounded-2xl p-8 border border-blue-200 dark:border-blue-800">
<h2 className="text-lg font-semibold mb-4">{t('adhd.singleTask.selectTask', 'Select a task to focus on')}</h2>
<Select value={selectedTaskId || ''} onValueChange={setSelectedTaskId}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('adhd.singleTask.choosePlaceholder', 'Choose a task...')} />
</SelectTrigger>
<SelectContent>
{activeTasks.map(task => (
<SelectItem key={task.id} value={task.id}>
{task.title}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="mt-6 flex gap-3">
<Button
variant="outline"
onClick={() => setShowFiveMinStarter(true)}
className="flex items-center gap-2"
>
<Timer className="h-4 w-4" />
{t('adhd.fiveMin.title')}
</Button>
</div>
</div>
) : (
<SingleTaskView
task={selectedTask}
onComplete={handleTaskComplete}
onExit={() => setSelectedTaskId(null)}
onUpdateTime={(taskId, seconds) => handleTaskUpdate(taskId, { timeTracked: seconds })}
/>
)}
{showFiveMinStarter && selectedTask && (
<FiveMinuteStarter
task={selectedTask}
onComplete={handleFiveMinComplete}
onCancel={() => setShowFiveMinStarter(false)}
/>
)}
</div>
);
}