feat: add social features, leaderboard, auth enhancements, and admin fixes
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Implement Social Features: Shared Tasks, Global Access, Privacy Settings (Leaderboard/Searchable). - Add Leaderboard Page and API. - Enhance Auth: Support Email/Username login, explicit duplicate registration errors. - Fix: Admin login password hash regression. - Refactor: Move to wouter for routing, add Admin Dashboard and User Management. - Add Setup Wizard. - Update UI with Sidebar and Gamification elements.
This commit is contained in:
+235
-300
@@ -1,11 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { queryClient } from "./lib/queryClient";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -16,122 +13,75 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import logoImage from '@assets/generated_images/TaskFlow_app_logo_icon_3a0e77e9.png';
|
||||
|
||||
import { Switch, Route, useLocation } from "wouter";
|
||||
|
||||
// Components
|
||||
import BottomNavigation from './components/BottomNavigation';
|
||||
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 ThemeToggle from './components/ThemeToggle';
|
||||
import Settings from './pages/settings';
|
||||
import AchievementsPage from './pages/AchievementsPage';
|
||||
import { Task, Label } from '@shared/schema';
|
||||
import { addDays, subDays } from 'date-fns';
|
||||
import { useTimer } from './hooks/useTimer';
|
||||
|
||||
// Mock data for prototype
|
||||
const initialTasks: Task[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Review quarterly reports',
|
||||
description: 'Analyze Q3 performance metrics and prepare summary',
|
||||
status: 'todo',
|
||||
priority: 'high',
|
||||
dueDate: addDays(new Date(), 2),
|
||||
timeTracked: 0,
|
||||
isTracking: false,
|
||||
projectId: 'project-1',
|
||||
notes: null,
|
||||
labelId: '2e8c3aa0-278f-4220-b2c5-aa109b4279b7' // Urgent label
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Update mobile app interface',
|
||||
description: 'Implement new design system and improve user experience',
|
||||
status: 'inProgress',
|
||||
priority: 'high',
|
||||
dueDate: addDays(new Date(), 1),
|
||||
timeTracked: 120,
|
||||
isTracking: true,
|
||||
projectId: 'project-2',
|
||||
notes: null,
|
||||
labelId: 'cb44bed1-8ba3-43fe-9498-bb28e483ed1f' // Work label
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Team standup meeting',
|
||||
description: null,
|
||||
status: 'todo',
|
||||
priority: 'low',
|
||||
dueDate: null,
|
||||
timeTracked: 0,
|
||||
isTracking: false,
|
||||
projectId: null,
|
||||
notes: null,
|
||||
labelId: '274f0ba4-a133-471a-bbe9-8189aa3b0106' // Personal label
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Fix critical login bug',
|
||||
description: 'Users unable to login with special characters in password',
|
||||
status: 'todo',
|
||||
priority: 'high',
|
||||
dueDate: null,
|
||||
timeTracked: 30,
|
||||
isTracking: false,
|
||||
projectId: 'project-2',
|
||||
notes: null,
|
||||
labelId: 'c5eccac9-7919-4eb1-bb50-badacad6b1c1' // Study label
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Deploy new features',
|
||||
description: null,
|
||||
status: 'done',
|
||||
priority: 'medium',
|
||||
dueDate: subDays(new Date(), 2),
|
||||
timeTracked: 120,
|
||||
isTracking: false,
|
||||
projectId: 'project-1',
|
||||
notes: null,
|
||||
labelId: null
|
||||
}
|
||||
];
|
||||
import { Plus } from 'lucide-react';
|
||||
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import { AppSidebar } from "./components/AppSidebar"
|
||||
import { CommandPalette } from './components/CommandPalette';
|
||||
import PomodoroOverlay from './components/PomodoroOverlay';
|
||||
import AuthPage from "@/pages/AuthPage";
|
||||
import SettingsPage from "@/pages/settings";
|
||||
import LeaderboardPage from "@/pages/LeaderboardPage";
|
||||
import SetupWizard from "@/pages/SetupWizard";
|
||||
import AdminUserManagement from "@/pages/AdminUserManagement";
|
||||
import NotFound from "@/pages/not-found";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { User } from "@shared/schema";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
function App() {
|
||||
const { t } = useTranslation();
|
||||
const [currentTab, setCurrentTab] = useState('tasks');
|
||||
const [tasks, setTasks] = useState<Task[]>(initialTasks);
|
||||
const [, setLocation] = useLocation();
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [labels, setLabels] = useState<Label[]>([]);
|
||||
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);
|
||||
|
||||
// Fetch tasks and labels from server on mount
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch tasks
|
||||
const tasksResponse = await fetch('/api/tasks');
|
||||
if (tasksResponse.ok) {
|
||||
const serverTasks: Task[] = await tasksResponse.json();
|
||||
// Normalize server tasks - convert date strings to Date objects
|
||||
const normalizedTasks = serverTasks.map(task => ({
|
||||
...task,
|
||||
dueDate: task.dueDate ? new Date(task.dueDate) : null
|
||||
}));
|
||||
// Merge server tasks with initial tasks (avoiding duplicates)
|
||||
const existingIds = new Set(normalizedTasks.map(t => t.id));
|
||||
const uniqueInitialTasks = initialTasks.filter(t => !existingIds.has(t.id));
|
||||
setTasks([...normalizedTasks, ...uniqueInitialTasks]);
|
||||
setTasks(normalizedTasks);
|
||||
}
|
||||
|
||||
// Fetch labels
|
||||
|
||||
const labelsResponse = await fetch('/api/labels');
|
||||
if (labelsResponse.ok) {
|
||||
const serverLabels: Label[] = await labelsResponse.json();
|
||||
@@ -139,11 +89,10 @@ function App() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
// Keep initial data on error
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [user]);
|
||||
|
||||
const handleCreateTask = async (newTask: Partial<Task>) => {
|
||||
try {
|
||||
@@ -160,22 +109,20 @@ function App() {
|
||||
isTracking: false,
|
||||
projectId: newTask.projectId || null,
|
||||
notes: newTask.notes || null,
|
||||
labelId: newTask.labelId || null
|
||||
labelId: newTask.labelId || null,
|
||||
energyLevel: newTask.energyLevel || 'medium',
|
||||
estimatedDuration: newTask.estimatedDuration || null
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create task');
|
||||
}
|
||||
|
||||
|
||||
if (!response.ok) throw new Error('Failed to create task');
|
||||
|
||||
const createdTask: Task = await response.json();
|
||||
// Normalize the created task's date
|
||||
const normalizedTask = {
|
||||
...createdTask,
|
||||
dueDate: createdTask.dueDate ? new Date(createdTask.dueDate) : null
|
||||
};
|
||||
setTasks(prev => [...prev, normalizedTask]);
|
||||
console.log('Task created:', normalizedTask);
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
}
|
||||
@@ -188,36 +135,36 @@ function App() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update task');
|
||||
}
|
||||
|
||||
|
||||
if (!response.ok) throw new Error('Failed to update task');
|
||||
|
||||
const updatedTask: Task = await response.json();
|
||||
// Normalize the updated task's date
|
||||
const normalizedTask = {
|
||||
...updatedTask,
|
||||
dueDate: updatedTask.dueDate ? new Date(updatedTask.dueDate) : null
|
||||
};
|
||||
setTasks(prev => prev.map(task =>
|
||||
task.id === taskId ? normalizedTask : task
|
||||
));
|
||||
console.log('Task updated:', taskId, updates);
|
||||
setTasks(prev => prev.map(task => task.id === taskId ? normalizedTask : task));
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error);
|
||||
// Optimistic update fallback
|
||||
setTasks(prev => prev.map(task =>
|
||||
task.id === taskId ? { ...task, ...updates } : task
|
||||
));
|
||||
setTasks(prev => prev.map(task => task.id === taskId ? { ...task, ...updates } : task));
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the timer hook after handleTaskUpdate is defined
|
||||
const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
|
||||
console.log('Creating project from template:', { templateId, startDate, projectName });
|
||||
};
|
||||
|
||||
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 });
|
||||
};
|
||||
@@ -229,7 +176,6 @@ function App() {
|
||||
const handleTaskClick = (task: Task) => {
|
||||
setSelectedTask(task);
|
||||
setIsTaskDetailsOpen(true);
|
||||
console.log('Task clicked:', task.title);
|
||||
};
|
||||
|
||||
const handleTaskDetailsClose = () => {
|
||||
@@ -239,7 +185,6 @@ function App() {
|
||||
|
||||
const handleTaskDetailsSave = (updatedTask: Task) => {
|
||||
handleTaskUpdate(updatedTask.id, updatedTask);
|
||||
console.log('Task details saved:', updatedTask.title);
|
||||
};
|
||||
|
||||
const handleTaskDelete = async (taskId: string) => {
|
||||
@@ -248,215 +193,205 @@ function App() {
|
||||
|
||||
const confirmTaskDelete = async () => {
|
||||
if (!deleteTaskId) return;
|
||||
|
||||
// Optimistic deletion - remove from UI immediately
|
||||
const taskToDelete = tasks.find(task => task.id === deleteTaskId);
|
||||
setTasks(prev => prev.filter(task => task.id !== deleteTaskId));
|
||||
|
||||
// Close task details modal if the deleted task was open
|
||||
|
||||
if (selectedTask?.id === deleteTaskId) {
|
||||
handleTaskDetailsClose();
|
||||
}
|
||||
|
||||
setDeleteTaskId(null);
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${deleteTaskId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
// 404 is acceptable - task doesn't exist on backend (seeded task)
|
||||
if (!response.ok && response.status !== 404) {
|
||||
throw new Error('Failed to delete task');
|
||||
}
|
||||
|
||||
console.log('Task deleted:', deleteTaskId);
|
||||
const response = await fetch(`/api/tasks/${deleteTaskId}`, { method: 'DELETE' });
|
||||
if (!response.ok && response.status !== 404) throw new Error('Failed to delete task');
|
||||
} catch (error) {
|
||||
console.error('Error deleting task:', error);
|
||||
// Restore task on error
|
||||
if (taskToDelete) {
|
||||
setTasks(prev => [...prev, taskToDelete]);
|
||||
}
|
||||
if (taskToDelete) setTasks(prev => [...prev, taskToDelete]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
|
||||
console.log('Creating project from template:', { templateId, startDate, projectName });
|
||||
// In a real app, this would create multiple tasks based on the template
|
||||
};
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
switch (currentTab) {
|
||||
case 'tasks':
|
||||
return (
|
||||
<TasksWithCalendar
|
||||
tasks={tasks}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskEdit={handleTaskClick}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onStartTimer={startTimer}
|
||||
onStopTimer={stopTimer}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'calendar':
|
||||
return (
|
||||
<CalendarView
|
||||
tasks={tasks}
|
||||
onTaskDrop={handleTaskDrop}
|
||||
onTaskClick={handleTaskClick}
|
||||
onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskEdit={handleTaskClick}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onStartTimer={startTimer}
|
||||
onStopTimer={stopTimer}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'kanban':
|
||||
return (
|
||||
<KanbanBoard
|
||||
tasks={tasks}
|
||||
onTaskStatusChange={handleTaskStatusChange}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskClick={handleTaskClick}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onStartTimer={startTimer}
|
||||
onStopTimer={stopTimer}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'weeklist':
|
||||
return (
|
||||
<WeekListView
|
||||
tasks={tasks}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskEdit={handleTaskClick}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onStartTimer={startTimer}
|
||||
onStopTimer={stopTimer}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'templates':
|
||||
return (
|
||||
<ProjectTemplate
|
||||
onCreateFromTemplate={handleCreateFromTemplate}
|
||||
onCreateTasks={(newTasks) => setTasks(prev => [...prev, ...newTasks])}
|
||||
onNavigateToSettings={() => setCurrentTab('settings')}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'settings':
|
||||
return <Settings onNavigateToTemplates={() => setCurrentTab('templates')} />;
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-muted-foreground">Page not found</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
// Setup Redirect
|
||||
if (setupStatus && !setupStatus.isSetup) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/setup" component={SetupWizard} />
|
||||
<Route component={() => <SetupWizard />} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <AuthPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex h-16 items-center justify-between px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={logoImage}
|
||||
alt="TaskFlow Logo"
|
||||
className="w-8 h-8 object-contain rounded-lg"
|
||||
data-testid="img-app-logo"
|
||||
/>
|
||||
<h1 className="text-lg font-semibold" data-testid="text-app-title">
|
||||
{t('app.title')}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{(currentTab === 'settings' || currentTab === 'templates') && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentTab('tasks')}
|
||||
data-testid="button-back-to-tasks"
|
||||
>
|
||||
{t('app.backToTasks')}
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<SidebarProvider>
|
||||
<AppSidebar user={user} />
|
||||
<SidebarInset>
|
||||
<div className="flex flex-col min-h-screen bg-background">
|
||||
{/* Mobile Header trigger */}
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 md:hidden">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<div className="font-semibold">{t('app.title')}</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<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="/tasks">
|
||||
<TasksWithCalendar
|
||||
tasks={tasks}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskEdit={handleTaskClick}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onStartTimer={startTimer}
|
||||
onStopTimer={stopTimer}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/calendar">
|
||||
<CalendarView
|
||||
tasks={tasks}
|
||||
onTaskDrop={handleTaskDrop}
|
||||
onTaskClick={handleTaskClick}
|
||||
onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskEdit={handleTaskClick}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onStartTimer={startTimer}
|
||||
onStopTimer={stopTimer}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/kanban">
|
||||
<KanbanBoard
|
||||
tasks={tasks}
|
||||
onTaskStatusChange={handleTaskStatusChange}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskClick={handleTaskClick}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onStartTimer={startTimer}
|
||||
onStopTimer={stopTimer}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/weeklist">
|
||||
<WeekListView
|
||||
tasks={tasks}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskEdit={handleTaskClick}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
onStartTimer={startTimer}
|
||||
onStopTimer={stopTimer}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/templates">
|
||||
<ProjectTemplate
|
||||
onCreateFromTemplate={handleCreateFromTemplate}
|
||||
onCreateTasks={(newTasks) => setTasks(prev => [...prev, ...newTasks])}
|
||||
onNavigateToSettings={() => setLocation('/settings')}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/achievements">
|
||||
<AchievementsPage user={user} />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
|
||||
</Route>
|
||||
|
||||
{/* Admin Route */}
|
||||
{user.role === 'admin' && (
|
||||
<Route path="/admin/users" component={AdminUserManagement} />
|
||||
)}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</header>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="pb-20 px-3 py-4 sm:px-4 sm:py-6 max-w-screen-2xl mx-auto">
|
||||
{renderContent()}
|
||||
</main>
|
||||
<CommandPalette
|
||||
onNavigate={(path) => setLocation(path.startsWith('/') ? path : `/${path}`)}
|
||||
onCreateTask={() => setIsCreateModalOpen(true)}
|
||||
/>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<BottomNavigation
|
||||
activeTab={currentTab}
|
||||
onTabChange={(tab) => {
|
||||
if (tab === 'settings') {
|
||||
setCurrentTab('settings');
|
||||
} else {
|
||||
setCurrentTab(tab);
|
||||
}
|
||||
}}
|
||||
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 })}
|
||||
/>
|
||||
|
||||
{/* Task Creation Modal */}
|
||||
<TaskCreationModal
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onSave={handleCreateTask}
|
||||
/>
|
||||
<Toaster />
|
||||
|
||||
{/* Task Details Modal */}
|
||||
<TaskDetailsModal
|
||||
isOpen={isTaskDetailsOpen}
|
||||
onClose={handleTaskDetailsClose}
|
||||
task={selectedTask}
|
||||
onSave={handleTaskDetailsSave}
|
||||
labels={labels}
|
||||
/>
|
||||
<TaskCreationModal
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onSave={handleCreateTask}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<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>
|
||||
|
||||
<Toaster />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
<TaskDetailsModal
|
||||
isOpen={isTaskDetailsOpen}
|
||||
onClose={handleTaskDetailsClose}
|
||||
task={selectedTask}
|
||||
onSave={handleTaskDetailsSave}
|
||||
labels={labels}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user