f91978c078
continuous-integration/drone/push Build is failing
- Add PWA manifest and service worker for push notifications
- Implement VAPID key generation and push subscription management
- Add push notification API endpoints (/api/push/*)
- Add push_subscriptions table to database schema
- Update notification settings UI with push support and iOS hints
- Fix translation issue showing "{ task } created" - add missing keys
- Fix mobile sidebar visibility for iOS home screen app
- Change default task filter from "all" to "open" (excludes done tasks)
- Add "Open" filter option to show only todo + inProgress tasks
557 lines
20 KiB
TypeScript
557 lines
20 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Toaster } from "@/components/ui/toaster";
|
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
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';
|
|
import TasksWithCalendar from './components/TasksWithCalendar';
|
|
import FocusMode from './components/FocusMode';
|
|
import CalendarView from './components/CalendarView';
|
|
import KanbanBoard from './components/KanbanBoard';
|
|
import ProjectTemplate from './components/ProjectTemplate';
|
|
import WeekListView from './components/WeekListView';
|
|
import Settings from './pages/settings';
|
|
import AchievementsPage from './pages/AchievementsPage';
|
|
import UnscheduledTasksPage from './pages/UnscheduledTasksPage';
|
|
import { Task, Label } from '@shared/schema';
|
|
import { useTimer } from './hooks/useTimer';
|
|
import { Plus } from 'lucide-react';
|
|
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar"
|
|
import { AppSidebar } from "./components/AppSidebar"
|
|
import { useNotifications } from './hooks/use-notifications';
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { CommandPalette } from './components/CommandPalette';
|
|
import PomodoroOverlay from './components/PomodoroOverlay';
|
|
import AuthPage from "@/pages/AuthPage";
|
|
|
|
import LeaderboardPage from "@/pages/LeaderboardPage";
|
|
import SetupWizard from "@/pages/SetupWizard";
|
|
import AdminUserManagement from "@/pages/AdminUserManagement";
|
|
import AdminSettings from "@/pages/AdminSettings";
|
|
import NotFound from "@/pages/not-found";
|
|
// import { AiChat } from "@/components/AiChat";
|
|
import ForgotPasswordPage from "@/pages/ForgotPasswordPage";
|
|
import ResetPasswordPage from "@/pages/ResetPasswordPage";
|
|
import AiChatPage from "@/pages/AiChatPage";
|
|
import NotificationsPage from "@/pages/NotificationsPage";
|
|
import FocusRoutinePage from "@/pages/FocusRoutinePage";
|
|
|
|
|
|
import { useRoutineBlocker } from './components/RoutineBlocker';
|
|
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { User } from "@shared/schema";
|
|
import { Loader2 } from "lucide-react";
|
|
|
|
|
|
import TimeTrackingPage from './pages/TimeTrackingPage';
|
|
|
|
// 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);
|
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
|
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null);
|
|
const [showPomodoro, setShowPomodoro] = useState(false);
|
|
const [activePomodoroTaskId, setActivePomodoroTaskId] = useState<string | null>(null);
|
|
|
|
// Enable global notifications polling
|
|
useNotifications({ poll: true });
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: user, isLoading: isLoadingUser } = useQuery<User>({
|
|
queryKey: ["/api/user"],
|
|
retry: false,
|
|
});
|
|
|
|
const { data: setupStatus } = useQuery<{ isSetup: boolean }>({
|
|
queryKey: ['/api/setup/status'],
|
|
});
|
|
|
|
// Fetch tasks and labels using React Query
|
|
const { data: tasksData } = useQuery<Task[]>({
|
|
queryKey: ['/api/tasks'],
|
|
enabled: !!user,
|
|
});
|
|
|
|
const tasks = (tasksData ?? []).map(task => ({
|
|
...task,
|
|
dueDate: task.dueDate ? new Date(task.dueDate) : null
|
|
}));
|
|
|
|
const { data: labelsData } = useQuery<Label[]>({
|
|
queryKey: ['/api/labels'],
|
|
enabled: !!user,
|
|
});
|
|
const labels = labelsData ?? [];
|
|
|
|
const handleCreateTask = async (newTask: Partial<Task>) => {
|
|
try {
|
|
const response = await fetch('/api/tasks', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title: newTask.title || '',
|
|
description: newTask.description || undefined,
|
|
status: 'todo',
|
|
priority: newTask.priority || 'medium',
|
|
dueDate: newTask.dueDate || null, // Explicitly handled as nullable in schema
|
|
timeTracked: 0,
|
|
isTracking: false,
|
|
projectId: newTask.projectId || undefined,
|
|
notes: newTask.notes || undefined,
|
|
labelId: newTask.labelId || undefined,
|
|
energyLevel: newTask.energyLevel || 'medium',
|
|
estimatedDuration: newTask.estimatedDuration || undefined
|
|
})
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to create task');
|
|
|
|
const createdTask: Task = await response.json();
|
|
|
|
// Update cache optimistically or invalidation
|
|
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => {
|
|
const normalizedTask = {
|
|
...createdTask,
|
|
dueDate: createdTask.dueDate ? new Date(createdTask.dueDate) : null
|
|
};
|
|
return [...old, normalizedTask];
|
|
});
|
|
// Invalidate to be sure
|
|
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
|
|
|
|
toast({
|
|
title: t('task.created', 'Task Created'),
|
|
description: t('task.createdDescription', '"{title}" has been added.', { title: createdTask.title }),
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error creating task:', error);
|
|
toast({
|
|
title: t('error.createTask', 'Error'),
|
|
description: t('error.createTaskDescription', 'Failed to create task. Please try again.'),
|
|
variant: "destructive"
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleTaskUpdate = async (taskId: string, updates: Partial<Task>) => {
|
|
// Optimistic Update
|
|
const previousTasks = queryClient.getQueryData<Task[]>(['/api/tasks']);
|
|
|
|
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) =>
|
|
old.map(task => task.id === taskId ? { ...task, ...updates } : task)
|
|
);
|
|
|
|
try {
|
|
const response = await fetch(`/api/tasks/${taskId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updates)
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to update task');
|
|
|
|
const updatedTask: Task = await response.json();
|
|
|
|
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => uniqueTasks(old, updatedTask));
|
|
|
|
} catch (error) {
|
|
console.error('Error updating task:', error);
|
|
// Rollback
|
|
if (previousTasks) {
|
|
queryClient.setQueryData(['/api/tasks'], previousTasks);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Helper to merge updated task into array
|
|
const uniqueTasks = (tasks: Task[], updated: Task) => {
|
|
const normalized = { ...updated, dueDate: updated.dueDate ? new Date(updated.dueDate) : null };
|
|
return tasks.map(t => t.id === updated.id ? normalized : t);
|
|
};
|
|
|
|
const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
|
|
console.log('Creating project from template:', { templateId, startDate, projectName });
|
|
// Assuming this might create tasks, we should invalidate
|
|
setTimeout(() => queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }), 1000);
|
|
};
|
|
|
|
const { startTimer, stopTimer } = useTimer({
|
|
tasks,
|
|
onTaskUpdate: handleTaskUpdate
|
|
});
|
|
|
|
const handleStartPomodoro = (taskId: string) => {
|
|
setActivePomodoroTaskId(taskId);
|
|
setShowPomodoro(true);
|
|
startTimer(taskId);
|
|
};
|
|
|
|
const handleTaskStatusChange = (taskId: string, newStatus: Task['status']) => {
|
|
handleTaskUpdate(taskId, { status: newStatus });
|
|
};
|
|
|
|
const handleTaskDrop = (taskId: string, newDate: Date) => {
|
|
handleTaskUpdate(taskId, { dueDate: newDate });
|
|
};
|
|
|
|
const handleTaskClick = (task: Task) => {
|
|
setSelectedTask(task);
|
|
setIsTaskDetailsOpen(true);
|
|
};
|
|
|
|
const handleTaskDetailsClose = () => {
|
|
setIsTaskDetailsOpen(false);
|
|
setSelectedTask(null);
|
|
};
|
|
|
|
const handleTaskDetailsSave = (updatedTask: Task) => {
|
|
handleTaskUpdate(updatedTask.id, updatedTask);
|
|
};
|
|
|
|
const handleTaskDelete = async (taskId: string) => {
|
|
setDeleteTaskId(taskId);
|
|
};
|
|
|
|
const confirmTaskDelete = async () => {
|
|
if (!deleteTaskId) return;
|
|
|
|
const previousTasks = queryClient.getQueryData<Task[]>(['/api/tasks']);
|
|
const taskToDelete = tasks.find(task => task.id === deleteTaskId);
|
|
|
|
// Optimistic delete
|
|
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) =>
|
|
old.filter(task => task.id !== deleteTaskId)
|
|
);
|
|
|
|
if (selectedTask?.id === deleteTaskId) {
|
|
handleTaskDetailsClose();
|
|
}
|
|
setDeleteTaskId(null);
|
|
|
|
try {
|
|
const response = await fetch(`/api/tasks/${deleteTaskId}`, { method: 'DELETE' });
|
|
if (!response.ok && response.status !== 404) throw new Error('Failed to delete task');
|
|
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
|
|
} catch (error) {
|
|
console.error('Error deleting task:', error);
|
|
// Rollback
|
|
if (previousTasks) {
|
|
queryClient.setQueryData(['/api/tasks'], previousTasks);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (isLoadingUser) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen bg-background">
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Setup Redirect
|
|
if (setupStatus && !setupStatus.isSetup) {
|
|
return (
|
|
<Switch>
|
|
<Route path="/setup" component={SetupWizard} />
|
|
<Route component={() => <SetupWizard />} />
|
|
</Switch>
|
|
);
|
|
}
|
|
|
|
|
|
if (isBlocked) {
|
|
return (
|
|
<div className="min-h-screen bg-background w-full">
|
|
<Switch>
|
|
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
|
|
{/* Catch all redirects to nothing, the hook handles the push to routine page */}
|
|
<Route component={() => <div />} />
|
|
</Switch>
|
|
<Toaster />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return (
|
|
<div className="min-h-screen bg-background w-full">
|
|
<Switch>
|
|
<Route path="/auth" component={AuthPage} />
|
|
<Route path="/achievements" component={AchievementsPage} />
|
|
<Route path="/leaderboard" component={LeaderboardPage} />
|
|
<Route path="/time-tracking" component={TimeTrackingPage} />
|
|
<Route path="/ai" component={AiChatPage} />
|
|
<Route component={AuthPage} />
|
|
</Switch>
|
|
<Toaster />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<SidebarProvider>
|
|
<AppSidebar user={user} />
|
|
<SidebarInset>
|
|
<div className={`min-h-screen bg-background flex flex-col ${adhdEnabled ? 'adhd-mode' : ''} ${adhdEnabled && adhdSettings.reducedAnimations ? 'reduced-motion' : ''} ${adhdEnabled && adhdSettings.largerTargets ? 'larger-targets' : ''}`}>
|
|
{/* Mobile Header with Sidebar Toggle */}
|
|
<header className="sticky top-0 z-50 flex items-center gap-3 px-4 py-3 bg-background/80 backdrop-blur-xl border-b border-border/50 md:hidden">
|
|
<SidebarTrigger className="h-9 w-9" />
|
|
<div className="flex items-center gap-2">
|
|
<img src="/favicon.png" alt="Logo" className="w-7 h-7 rounded-lg" />
|
|
<span className="font-bold text-lg">{t('app.title')}</span>
|
|
</div>
|
|
</header>
|
|
<main className="flex-1 p-4 md:p-6 max-w-screen-2xl mx-auto w-full relative">
|
|
<Switch>
|
|
<Route path="/">
|
|
<FocusMode
|
|
tasks={tasks}
|
|
onTaskUpdate={handleTaskUpdate}
|
|
onTaskEdit={handleTaskClick}
|
|
onTaskDelete={handleTaskDelete}
|
|
onStartTimer={startTimer}
|
|
onStopTimer={stopTimer}
|
|
onNavigateToTasks={() => setLocation('/tasks')}
|
|
/>
|
|
</Route>
|
|
<Route path="/focus">
|
|
<FocusMode
|
|
tasks={tasks}
|
|
onTaskUpdate={handleTaskUpdate}
|
|
onTaskEdit={handleTaskClick}
|
|
onTaskDelete={handleTaskDelete}
|
|
onStartTimer={startTimer}
|
|
onStopTimer={stopTimer}
|
|
onNavigateToTasks={() => setLocation('/tasks')}
|
|
/>
|
|
</Route>
|
|
<Route path="/calendar">
|
|
<CalendarView tasks={tasks} />
|
|
</Route>
|
|
<Route path="/weeklist">
|
|
<WeekListView tasks={tasks} onTaskUpdate={handleTaskUpdate} />
|
|
</Route>
|
|
<Route path="/kanban">
|
|
<KanbanBoard
|
|
tasks={tasks}
|
|
onTaskUpdate={handleTaskUpdate}
|
|
onTaskEdit={handleTaskClick}
|
|
onTaskDelete={handleTaskDelete}
|
|
/>
|
|
</Route>
|
|
<Route path="/auth">
|
|
{() => {
|
|
setLocation('/');
|
|
return null;
|
|
}}
|
|
</Route>
|
|
<Route path="/tasks">
|
|
<TasksWithCalendar
|
|
tasks={tasks}
|
|
onTaskUpdate={handleTaskUpdate}
|
|
onTaskEdit={handleTaskClick}
|
|
onTaskDelete={handleTaskDelete}
|
|
onStartTimer={startTimer}
|
|
onStopTimer={stopTimer}
|
|
/>
|
|
</Route>
|
|
<Route path="/templates">
|
|
<ProjectTemplate
|
|
onCreateFromTemplate={handleCreateFromTemplate}
|
|
onCreateTasks={(newTasks) => {
|
|
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
|
|
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
|
|
}}
|
|
onNavigateToSettings={() => setLocation('/settings')}
|
|
/>
|
|
</Route>
|
|
<Route path="/achievements">
|
|
<AchievementsPage user={user} />
|
|
</Route>
|
|
|
|
<Route path="/unscheduled">
|
|
<UnscheduledTasksPage
|
|
user={user}
|
|
onToggleCompletion={(taskId, currentStatus) => handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
|
|
onDelete={(id) => setDeleteTaskId(id)}
|
|
onUpdate={handleTaskUpdate}
|
|
onSelect={setSelectedTask}
|
|
/>
|
|
</Route>
|
|
<Route path="/leaderboard">
|
|
<LeaderboardPage />
|
|
</Route>
|
|
<Route path="/leaderboard">
|
|
<LeaderboardPage />
|
|
</Route>
|
|
<Route path="/ai">
|
|
<AiChatPage />
|
|
</Route>
|
|
<Route path="/notifications">
|
|
<NotificationsPage />
|
|
</Route>
|
|
<Route path="/time-tracking">
|
|
<TimeTrackingPage />
|
|
</Route>
|
|
<Route path="/settings">
|
|
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
|
|
</Route>
|
|
|
|
{/* Admin Route */}
|
|
{user?.role === 'admin' && (
|
|
<>
|
|
<Route path="/admin/users" component={AdminUserManagement} />
|
|
<Route path="/admin/settings" component={AdminSettings} />
|
|
</>
|
|
)}
|
|
|
|
<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>
|
|
|
|
{/* Floating Action Button */}
|
|
<div className="fixed bottom-8 right-8 z-[100]">
|
|
<Button
|
|
size="icon"
|
|
className="h-14 w-14 rounded-full shadow-2xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:scale-110 transition-transform duration-200"
|
|
onClick={() => setIsCreateModalOpen(true)}
|
|
data-testid="fab-create-task"
|
|
>
|
|
<Plus className="h-6 w-6 text-white" />
|
|
</Button>
|
|
</div>
|
|
<CommandPalette
|
|
onNavigate={(path) => setLocation(path.startsWith('/') ? path : `/${path}`)}
|
|
onCreateTask={() => setIsCreateModalOpen(true)}
|
|
/>
|
|
|
|
<PomodoroOverlay
|
|
isOpen={showPomodoro}
|
|
onClose={() => setShowPomodoro(false)}
|
|
taskId={activePomodoroTaskId}
|
|
taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'}
|
|
onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })}
|
|
/>
|
|
|
|
<Toaster />
|
|
|
|
{/* ADHD Mode Overlays */}
|
|
{adhdEnabled && <BreakReminderOverlay />}
|
|
{adhdEnabled && adhdSettings.hyperfocusProtection && <HyperfocusGuard maxMinutes={adhdSettings.hyperfocusMaxMinutes} />}
|
|
{adhdEnabled && adhdSettings.positiveMessagingLevel !== 'off' && <EncouragementToast />}
|
|
|
|
<TaskCreationModal
|
|
isOpen={isCreateModalOpen}
|
|
onClose={() => setIsCreateModalOpen(false)}
|
|
onSave={handleCreateTask}
|
|
/>
|
|
|
|
<TaskDetailsModal
|
|
isOpen={isTaskDetailsOpen}
|
|
onClose={handleTaskDetailsClose}
|
|
task={selectedTask}
|
|
onSave={handleTaskDetailsSave}
|
|
labels={labels}
|
|
onNavigate={(id) => {
|
|
const t = tasks.find(x => x.id === id);
|
|
if (t) setSelectedTask(t);
|
|
}}
|
|
/>
|
|
|
|
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>
|
|
<AlertDialogContent data-testid="dialog-delete-confirmation">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{t('deleteConfirmation.title')}</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{t('deleteConfirmation.description')}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel data-testid="button-cancel-delete">
|
|
{t('deleteConfirmation.cancel')}
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={confirmTaskDelete}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
data-testid="button-confirm-delete"
|
|
>
|
|
{t('deleteConfirmation.confirm')}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</SidebarInset>
|
|
</SidebarProvider>
|
|
</TooltipProvider>
|
|
);
|
|
|
|
|
|
}
|
|
|
|
// Main App wrapper with ADHD providers
|
|
function App() {
|
|
return (
|
|
<ADHDModeProvider>
|
|
<BreakReminderProvider>
|
|
<AppContent />
|
|
</BreakReminderProvider>
|
|
</ADHDModeProvider>
|
|
);
|
|
}
|
|
|
|
export default App;
|
|
|