feat: Add Focus Tools (ADHD-friendly productivity features)
continuous-integration/drone/push Build is passing
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:
+47
-2
@@ -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;
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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!"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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%
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user