diff --git a/.env b/.env new file mode 100644 index 0000000..1b133ae --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=postgresql://taskflow:taskflow_password@localhost:5432/taskflow diff --git a/clear_sessions.sql b/clear_sessions.sql new file mode 100644 index 0000000..a00921c --- /dev/null +++ b/clear_sessions.sql @@ -0,0 +1 @@ +TRUNCATE TABLE "session"; diff --git a/client/index.html b/client/index.html index e7e6b58..18597a5 100644 --- a/client/index.html +++ b/client/index.html @@ -1,20 +1,24 @@ - - - TaskFlow - Personal Task Management - - - - - - - - - -
- - - - + + + + TaskFlow - Personal Task Management + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index a979f79..26ddcfc 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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(initialTasks); + const [, setLocation] = useLocation(); + const [tasks, setTasks] = useState([]); const [labels, setLabels] = useState([]); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false); const [selectedTask, setSelectedTask] = useState(null); const [deleteTaskId, setDeleteTaskId] = useState(null); + const [showPomodoro, setShowPomodoro] = useState(false); + const [activePomodoroTaskId, setActivePomodoroTaskId] = useState(null); - // Fetch tasks and labels from server on mount + const { data: user, isLoading: isLoadingUser } = useQuery({ + 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) => { 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 ( +
+ +
+ ); + } - const renderContent = () => { - switch (currentTab) { - case 'tasks': - return ( - - ); - - case 'calendar': - return ( - console.log('Date selected:', date.toLocaleDateString())} - onTaskUpdate={handleTaskUpdate} - onTaskEdit={handleTaskClick} - onTaskDelete={handleTaskDelete} - onStartTimer={startTimer} - onStopTimer={stopTimer} - /> - ); - - case 'kanban': - return ( - - ); - - case 'weeklist': - return ( - - ); - - case 'templates': - return ( - setTasks(prev => [...prev, ...newTasks])} - onNavigateToSettings={() => setCurrentTab('settings')} - /> - ); - - case 'settings': - return setCurrentTab('templates')} />; - - default: - return ( -
-

Page not found

-
- ); - } - }; + // Setup Redirect + if (setupStatus && !setupStatus.isSetup) { + return ( + + + } /> + + ); + } + + if (!user) { + return ; + } return ( - - -
- {/* Header */} -
-
-
- TaskFlow Logo -

- {t('app.title')} -

-
- -
- {(currentTab === 'settings' || currentTab === 'templates') && ( - + + + + +
+ {/* Mobile Header trigger */} +
+ +
{t('app.title')}
+
+ + {/* Main Content */} +
+ + + setLocation('/tasks')} + /> + + + + + + console.log('Date selected:', date.toLocaleDateString())} + onTaskUpdate={handleTaskUpdate} + onTaskEdit={handleTaskClick} + onTaskDelete={handleTaskDelete} + onStartTimer={startTimer} + onStopTimer={stopTimer} + /> + + + + + + + + + setTasks(prev => [...prev, ...newTasks])} + onNavigateToSettings={() => setLocation('/settings')} + /> + + + + + + setLocation('/templates')} /> + + + {/* Admin Route */} + {user.role === 'admin' && ( + )} - -
+ + + + + + {/* Floating Action Button */} +
+
-
+
+ + - {/* Main Content */} -
- {renderContent()} -
+ setLocation(path.startsWith('/') ? path : `/${path}`)} + onCreateTask={() => setIsCreateModalOpen(true)} + /> - {/* Bottom Navigation */} - { - if (tab === 'settings') { - setCurrentTab('settings'); - } else { - setCurrentTab(tab); - } - }} - onCreateTask={() => setIsCreateModalOpen(true)} - /> + 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 */} - setIsCreateModalOpen(false)} - onSave={handleCreateTask} - /> + - {/* Task Details Modal */} - + setIsCreateModalOpen(false)} + onSave={handleCreateTask} + /> - {/* Delete Confirmation Dialog */} - !open && setDeleteTaskId(null)}> - - - {t('deleteConfirmation.title')} - - {t('deleteConfirmation.description')} - - - - - {t('deleteConfirmation.cancel')} - - - {t('deleteConfirmation.confirm')} - - - - - - - -
-
+ + + !open && setDeleteTaskId(null)}> + + + {t('deleteConfirmation.title')} + + {t('deleteConfirmation.description')} + + + + + {t('deleteConfirmation.cancel')} + + + {t('deleteConfirmation.confirm')} + + + + + ); } diff --git a/client/src/components/AppSidebar.tsx b/client/src/components/AppSidebar.tsx new file mode 100644 index 0000000..20c3a4f --- /dev/null +++ b/client/src/components/AppSidebar.tsx @@ -0,0 +1,120 @@ +import { useTranslation } from 'react-i18next'; +import { Home, Calendar, List, LayoutGrid, Settings, CheckSquare, Target, Trophy, LogOut, Award } from 'lucide-react'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuItem, + SidebarMenuButton, + SidebarRail, + useSidebar, + SidebarTrigger, +} from "@/components/ui/sidebar" +import { useQueryClient, useMutation } from '@tanstack/react-query'; +import { useToast } from "@/hooks/use-toast"; +import { User } from "@shared/schema"; +import ThemeToggle from './ThemeToggle'; +import { useLocation } from "wouter"; + +import { GamificationBar } from './GamificationBar'; + +interface AppSidebarProps extends React.ComponentProps { + user: User | undefined; +} + +export function AppSidebar({ user, ...props }: AppSidebarProps) { + const { t } = useTranslation(); + const { state } = useSidebar(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [location, setLocation] = useLocation(); + + const logoutMutation = useMutation({ + mutationFn: async () => { + await fetch("/api/logout", { method: "POST" }); + }, + onSuccess: () => { + queryClient.setQueryData(["/api/user"], null); + toast({ title: "Logged out successfully" }); + }, + }); + + const items = [ + { title: t('navigation.focus'), id: 'focus', path: '/', icon: Target, color: 'text-red-500' }, + { title: t('navigation.tasks'), id: 'tasks', path: '/tasks', icon: Home, color: 'text-blue-500' }, + { title: t('navigation.calendar'), id: 'calendar', path: '/calendar', icon: Calendar, color: 'text-violet-500' }, + { title: t('navigation.weekList'), id: 'weeklist', path: '/weeklist', icon: List, color: 'text-pink-500' }, + { title: t('navigation.kanban'), id: 'kanban', path: '/kanban', icon: LayoutGrid, color: 'text-orange-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('navigation.settings'), id: 'settings', path: '/settings', icon: Settings, color: 'text-gray-500' }, + ] + + return ( + + + + + +
+ +
+ {state !== 'collapsed' && ( +
+ {t('app.title')} + Personal +
+ )} +
+
+
+
+ + + {items.map((item) => ( + + setLocation(item.path)} + tooltip={item.title} + className={`h-12 transition-all duration-200 ${location === item.path || (item.path !== '/' && location.startsWith(item.path)) + ? 'bg-gradient-to-r from-violet-600 to-indigo-600 text-white shadow-md hover:from-violet-500 hover:to-indigo-500 hover:text-white' + : 'hover:bg-sidebar-accent hover:pl-4'}`} + > + + {state !== 'collapsed' && ( + {item.title} + )} + + + ))} + + + + {state !== 'collapsed' && user && ( + + )} + + + logoutMutation.mutate()} + disabled={logoutMutation.isPending} + className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20" + > + + {state !== 'collapsed' && Logout} + + + +
+ + +
+
+ +
+ ) +} + diff --git a/client/src/components/BottomNavigation.tsx b/client/src/components/BottomNavigation.tsx deleted file mode 100644 index fdb8ad0..0000000 --- a/client/src/components/BottomNavigation.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Home, Calendar, List, LayoutGrid, Plus, Settings } from 'lucide-react'; -import { useState } from 'react'; - -interface BottomNavigationProps { - onTabChange?: (tab: string) => void; - onCreateTask?: () => void; - activeTab?: string; -} - -export default function BottomNavigation({ onTabChange, onCreateTask, activeTab = 'tasks' }: BottomNavigationProps) { - const { t } = useTranslation(); - const [currentTab, setCurrentTab] = useState(activeTab); - - const tabs = [ - { id: 'tasks', label: t('navigation.tasks'), icon: Home }, - { id: 'calendar', label: t('navigation.calendar'), icon: Calendar }, - { id: 'weeklist', label: t('navigation.weekList'), icon: List }, - { id: 'create', label: t('navigation.create'), icon: Plus, isCreate: true }, - { id: 'kanban', label: t('navigation.kanban'), icon: LayoutGrid }, - { id: 'settings', label: t('navigation.settings'), icon: Settings } - ]; - - const handleTabClick = (tabId: string, isCreate?: boolean) => { - if (isCreate) { - onCreateTask?.(); - console.log('Create task triggered'); - return; - } - - setCurrentTab(tabId); - onTabChange?.(tabId); - console.log('Tab changed to:', tabId); - }; - - return ( -
-
- {tabs.map((tab) => { - const isActive = currentTab === tab.id && !tab.isCreate; - const Icon = tab.icon; - - return ( - - ); - })} -
-
- ); -} \ No newline at end of file diff --git a/client/src/components/CalendarView.tsx b/client/src/components/CalendarView.tsx index 5def077..a1039eb 100644 --- a/client/src/components/CalendarView.tsx +++ b/client/src/components/CalendarView.tsx @@ -60,9 +60,11 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel const dates = Array.from({ length: 35 }, (_, i) => addDays(startDate, i)); const getTasksForDate = (date: Date) => { - return tasks.filter(task => - task.dueDate && isSameDay(task.dueDate, date) - ); + return tasks.filter(task => { + if (!task.dueDate) return false; + const taskDate = new Date(task.dueDate); + return isSameDay(taskDate, date); + }); }; const handleDragStart = (taskId: string) => { @@ -98,15 +100,15 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel if (longPressTimerRef.current) { clearTimeout(longPressTimerRef.current); } - + longPressTriggered.current = false; - + longPressTimerRef.current = setTimeout(() => { longPressTriggered.current = true; setLongPressTask(task); setDropdownOpen(true); console.log('Long press detected for task:', task.title); - + // Reset after a delay to allow next click setTimeout(() => { longPressTriggered.current = false; @@ -124,13 +126,13 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel const handleTaskClick = (task: Task, e: React.MouseEvent | React.TouchEvent) => { // Prevent click when dragging if (draggedTask === task.id) return; - + // Skip click if long press was just triggered if (longPressTriggered.current) { e.preventDefault(); return; } - + e.stopPropagation(); onTaskClick?.(task); console.log('Calendar task clicked:', task.title); @@ -139,7 +141,7 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel // Menu content component to avoid duplication const TaskMenuContent = ({ task, closeMenu }: { task: Task; closeMenu?: () => void }) => ( <> - { e.stopPropagation(); onTaskEdit?.(task); @@ -150,8 +152,8 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel {t('taskCard.editTask')} - - { e.stopPropagation(); if (task.isTracking) { @@ -166,11 +168,11 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel {task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')} - + - + {task.status !== 'done' && ( - { e.stopPropagation(); onTaskUpdate?.(task.id, { status: 'done' }); @@ -182,9 +184,9 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel {t('taskCard.markAsDone')} )} - + {task.status === 'todo' && ( - { e.stopPropagation(); onTaskUpdate?.(task.id, { status: 'inProgress' }); @@ -196,9 +198,9 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel {t('taskCard.startWorking')} )} - + {task.status === 'inProgress' && ( - { e.stopPropagation(); onTaskUpdate?.(task.id, { status: 'todo' }); @@ -210,9 +212,9 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel {t('taskCard.moveToTodo')} )} - + {task.status === 'done' && ( - { e.stopPropagation(); onTaskUpdate?.(task.id, { status: 'inProgress' }); @@ -224,11 +226,11 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel {t('taskCard.reopenTask')} )} - + - + {onTaskDelete && ( - { e.stopPropagation(); onTaskDelete(task.id); @@ -247,18 +249,21 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel return (
{/* Header */} -
+

{t('calendar.title')}

+ + {t('analytics.cw')} {format(currentDate, 'w', { locale: dateLocale })} +
- +
- - - + + +
+ + +
+

+ + {t('focus.list.title')} +

+ {t('focus.list.subtitle')} +
+ + {focusTasks.length > 0 ? ( + + t.id)} + strategy={verticalListSortingStrategy} + > +
+ {focusTasks.map(task => ( + + ))} +
+
+
+ ) : ( + +

{t('focus.list.empty')}

+
+ )} +
+
+ ); +} diff --git a/client/src/components/GamificationBar.tsx b/client/src/components/GamificationBar.tsx new file mode 100644 index 0000000..ed471a2 --- /dev/null +++ b/client/src/components/GamificationBar.tsx @@ -0,0 +1,74 @@ +import { Trophy, Flame } from 'lucide-react'; +import { Progress } from "@/components/ui/progress"; +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import { + getLevelFromXP, + getNextLevelXP, + getLevelProgress, + getRankKey +} from "@/lib/gamification"; +import { LevelDetailsModal } from './LevelDetailsModal'; + +interface GamificationBarProps { + xp: number; + level: number; // Keeping prop for backwards compat, but should calculate from XP usually + streak: number; +} + +export function GamificationBar({ xp, streak }: GamificationBarProps) { + const { t } = useTranslation(); + const [isModalOpen, setIsModalOpen] = useState(false); + + // Use centralized logic + const computedLevel = getLevelFromXP(xp); + const nextLevelXP = getNextLevelXP(computedLevel); + const progress = getLevelProgress(xp); + const rankKey = getRankKey(computedLevel); + + return ( + <> +
setIsModalOpen(true)} + > +
+
+
+ +
+
+ + {t('gamification.level', { level: computedLevel })} + +
+ {t(`ranks.${rankKey}`)} +
+
+
+
+ + + {t('gamification.streak', { count: streak })} + +
+
+ +
+
+ {t('gamification.xp', { count: xp })} + {t('gamification.nextLevel', { count: nextLevelXP })} +
+ +
+
+ + setIsModalOpen(false)} + xp={xp} + /> + + ); +} + diff --git a/client/src/components/KanbanBoard.tsx b/client/src/components/KanbanBoard.tsx index 9ce2f1a..e525655 100644 --- a/client/src/components/KanbanBoard.tsx +++ b/client/src/components/KanbanBoard.tsx @@ -42,7 +42,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o const [viewMode, setViewMode] = useState('traditional'); const [currentDate, setCurrentDate] = useState(new Date()); const [draggedTask, setDraggedTask] = useState(null); - + // Filter state const [selectedLabels, setSelectedLabels] = useState([]); const [selectedPriorities, setSelectedPriorities] = useState([]); @@ -65,36 +65,36 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o const getTasksForColumn = (status: Task['status']) => { let filteredTasks = tasks.filter(task => task.status === status); - + // Apply label filtering if (selectedLabels.length > 0) { - filteredTasks = filteredTasks.filter(task => + filteredTasks = filteredTasks.filter(task => task.labelId && selectedLabels.includes(task.labelId) ); } - + // Apply priority filtering if (selectedPriorities.length > 0) { - filteredTasks = filteredTasks.filter(task => + filteredTasks = filteredTasks.filter(task => selectedPriorities.includes(task.priority) ); } - + // Apply date filtering for weekly/monthly views if (viewMode === 'weekly') { const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 }); // Monday = 1 const weekEnd = endOfWeek(currentDate, { weekStartsOn: 1 }); - filteredTasks = filteredTasks.filter(task => + filteredTasks = filteredTasks.filter(task => task.dueDate && isWithinInterval(task.dueDate, { start: weekStart, end: weekEnd }) ); } else if (viewMode === 'monthly') { const monthStart = startOfMonth(currentDate); const monthEnd = endOfMonth(currentDate); - filteredTasks = filteredTasks.filter(task => + filteredTasks = filteredTasks.filter(task => task.dueDate && isWithinInterval(task.dueDate, { start: monthStart, end: monthEnd }) ); } - + return filteredTasks; }; @@ -139,7 +139,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o // Filter management functions const toggleLabel = (labelId: string) => { - setSelectedLabels(prev => + setSelectedLabels(prev => prev.includes(labelId) ? prev.filter(id => id !== labelId) : [...prev, labelId] @@ -147,7 +147,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o }; const togglePriority = (priority: string) => { - setSelectedPriorities(prev => + setSelectedPriorities(prev => prev.includes(priority) ? prev.filter(p => p !== priority) : [...prev, priority] @@ -175,7 +175,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o return (
{/* Header */} -
+
@@ -183,11 +183,11 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o {t('kanban.title')}
- + {viewMode !== 'traditional' && (
- - + {getPeriodTitle()} - - - - -
-

{t('kanban.filterByLabels')}

- -
- {labels.length > 0 ? ( - labels.map((label) => ( -
+ {t('kanban.filters')} + {hasActiveFilters && ( + + {selectedLabels.length + selectedPriorities.length} + + )} +
+ +
+ {/* Labels Filter */} +
+ + + + + +
+

{t('kanban.filterByLabels')}

+ +
+ {labels.length > 0 ? ( + labels.map((label) => ( +
+ toggleLabel(label.id)} + data-testid={`checkbox-label-${label.id}`} + /> +
+
+ + +
+ + {/* Priorities Filter */} +
+ + + + + +
+

{t('kanban.filterByPriority')}

+ +
+ {priorities.map((priority) => ( +
toggleLabel(label.id)} - data-testid={`checkbox-label-${label.id}`} + id={`priority-${priority}`} + checked={selectedPriorities.includes(priority)} + onCheckedChange={() => togglePriority(priority)} + data-testid={`checkbox-priority-${priority}`} />
-
-
-
-
+
+
+
- {/* Priorities Filter */} -
- - - - - -
-

{t('kanban.filterByPriority')}

- -
- {priorities.map((priority) => ( -
- togglePriority(priority)} - data-testid={`checkbox-priority-${priority}`} - /> - -
- ))} -
-
-
-
+ + )}
- - {/* Clear Filters */} - {hasActiveFilters && ( - <> - - - - )}
-
- - {/* Active Filters Display */} - {hasActiveFilters && ( -
-
- {t('kanban.activeFilters')} - {selectedLabels.map((labelId) => { - const label = labels.find(l => l.id === labelId); - return label ? ( - +
+ {t('kanban.activeFilters')} + {selectedLabels.map((labelId) => { + const label = labels.find(l => l.id === labelId); + return label ? ( + +
+ {label.name} + toggleLabel(labelId)} + /> + + ) : null; + })} + {selectedPriorities.map((priority) => ( + -
- {label.name} - toggleLabel(labelId)} + {t(`priority.${priority}`)} + togglePriority(priority)} /> - ) : null; - })} - {selectedPriorities.map((priority) => ( - - {t(`priority.${priority}`)} - togglePriority(priority)} - /> - - ))} + ))} +
-
- )} - + )} +
{/* View Mode Tabs */} @@ -425,9 +425,9 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
{columns.map((column) => { const columnTasks = getTasksForColumn(column.status); - + return ( -
- +
{columnTasks.map((task) => (
- onStartTimer?.(task.id)} onStopTimer={() => onStopTimer?.(task.id)} @@ -472,7 +472,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o />
))} - + {columnTasks.length === 0 && (
{t('kanban.noTasksInColumn', { column: column.title.toLowerCase() })} @@ -489,9 +489,9 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
{columns.map((column) => { const columnTasks = getTasksForColumn(column.status); - + return ( -
- +
{columnTasks.map((task) => (
- onStartTimer?.(task.id)} onStopTimer={() => onStopTimer?.(task.id)} @@ -536,7 +536,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o />
))} - + {columnTasks.length === 0 && (
{t('kanban.noTasksInColumnForWeek', { column: column.title.toLowerCase() })} @@ -553,9 +553,9 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
{columns.map((column) => { const columnTasks = getTasksForColumn(column.status); - + return ( -
- +
{columnTasks.map((task) => (
- onStartTimer?.(task.id)} onStopTimer={() => onStopTimer?.(task.id)} @@ -600,7 +600,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o />
))} - + {columnTasks.length === 0 && (
{t('kanban.noTasksInColumnForMonth', { column: column.title.toLowerCase() })} diff --git a/client/src/components/LevelDetailsModal.tsx b/client/src/components/LevelDetailsModal.tsx new file mode 100644 index 0000000..230f207 --- /dev/null +++ b/client/src/components/LevelDetailsModal.tsx @@ -0,0 +1,96 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Progress } from "@/components/ui/progress"; +import { Trophy, Star, TrendingUp } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { + getLevelFromXP, + getNextLevelXP, + getLevelProgress, + getRankKey +} from "@/lib/gamification"; + +interface LevelDetailsModalProps { + isOpen: boolean; + onClose: () => void; + xp: number; +} + +export function LevelDetailsModal({ isOpen, onClose, xp }: LevelDetailsModalProps) { + const { t } = useTranslation(); + + const level = getLevelFromXP(xp); + const nextLevelXP = getNextLevelXP(level); + const progress = getLevelProgress(xp); + const rankKey = getRankKey(level); + + // Calculate XP needed for next level relative to current level start + // Note: For display, we usually show Total XP / Threshold or Current Level XP / needed + // Let's show Total XP / Next Threshold for clarity as typical in RPGs + + return ( + + + + + + {t("achievements.levelDetails")} + + + +
+ {/* Level Badge */} +
+
+ + {level} + +
+
+ {t(`gamification.level`, { level })} +
+
+ + {/* Rank Title */} +
+

+ {t(`ranks.${rankKey}`)} +

+

+ {t("achievements.xpDescription")} +

+
+ + {/* Progress Section */} +
+
+ + {t("gamification.currentXP", { current: xp, next: nextLevelXP })} + + + {Math.round(progress)}% + +
+ +
+ + {/* Benefits / Rewards Placeholder */} +
+

+ + {t("achievements.nextReward")} +

+
+ + {t('achievements.unlockReward', { level: level + 1 })} +
+
+
+
+
+ ); +} diff --git a/client/src/components/PomodoroOverlay.tsx b/client/src/components/PomodoroOverlay.tsx new file mode 100644 index 0000000..e2a6009 --- /dev/null +++ b/client/src/components/PomodoroOverlay.tsx @@ -0,0 +1,212 @@ +import { useState, useEffect, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Play, Pause, Volume2, VolumeX, SkipForward, CheckCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { ambientPlayer } from '@/lib/ambient-sounds'; +import { useTranslation } from 'react-i18next'; +import { triggerConfetti } from '@/lib/confetti'; +import { playSuccessSound } from '@/lib/sounds'; + +interface PomodoroOverlayProps { + taskId: string | null; + taskTitle: string; + isOpen: boolean; + onClose: () => void; + onCompleteTask: (taskId: string) => void; +} + +export default function PomodoroOverlay({ taskId, taskTitle, isOpen, onClose, onCompleteTask }: PomodoroOverlayProps) { + const { t } = useTranslation(); + const [timeLeft, setTimeLeft] = useState(25 * 60); + const [isActive, setIsActive] = useState(false); + const [isSoundPlaying, setIsSoundPlaying] = useState(false); + const [mode, setMode] = useState<'focus' | 'break'>('focus'); + + const workerRef = useRef(null); + + // Initialize Worker + useEffect(() => { + if (typeof Worker !== 'undefined') { + workerRef.current = new Worker(new URL('../workers/timer.worker.ts', import.meta.url)); + + workerRef.current.onmessage = (e) => { + const { type, remaining } = e.data; + if (type === 'TICK') { + setTimeLeft(remaining); + } else if (type === 'COMPLETE') { + setIsActive(false); + playSuccessSound(); + triggerConfetti(0.5, 0.5); + // If focus finished, propose break? + } + }; + } + + return () => { + workerRef.current?.terminate(); + ambientPlayer.stop(); + }; + }, []); + + // Timer Control + useEffect(() => { + if (isActive) { + workerRef.current?.postMessage({ + command: 'START', + payload: { durationSeconds: timeLeft } + }); + } else { + workerRef.current?.postMessage({ command: 'STOP' }); + } + }, [isActive]); + + const toggleTimer = () => setIsActive(!isActive); + + const toggleSound = () => { + const playing = ambientPlayer.toggle(); + setIsSoundPlaying(playing); + }; + + const skipTimer = () => { + setIsActive(false); + // Switch modes + if (mode === 'focus') { + setMode('break'); + setTimeLeft(5 * 60); + } else { + setMode('focus'); + setTimeLeft(25 * 60); + } + }; + + const handleComplete = () => { + if (taskId) { + onCompleteTask(taskId); + onClose(); + setIsActive(false); + ambientPlayer.stop(); + } + }; + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + }; + + // Circular Progress Calculation + const totalTime = mode === 'focus' ? 25 * 60 : 5 * 60; + const progress = ((totalTime - timeLeft) / totalTime) * 100; + const circleSize = 280; + const radius = 120; + const circumference = 2 * Math.PI * radius; + const strokeDashoffset = circumference - (progress / 100) * circumference; + + return ( + + {isOpen && ( + + + +
+
+

{mode === 'focus' ? 'Focus Time' : 'Break Time'}

+

{taskTitle}

+
+ +
+ {/* SVG Ring */} + + {/* Background Circle */} + + {/* Progress Circle */} + + + + {/* Time Display */} +
+ + {formatTime(timeLeft)} + +
+
+ +
+ + + + + +
+ + {taskId && mode === 'focus' && ( + + )} +
+
+ )} +
+ ); +} diff --git a/client/src/components/TaskCard.tsx b/client/src/components/TaskCard.tsx index e763336..3b60751 100644 --- a/client/src/components/TaskCard.tsx +++ b/client/src/components/TaskCard.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -20,11 +21,16 @@ import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, Chec import { Task, Label } from '@shared/schema'; import { useQuery } from '@tanstack/react-query'; import { Checkbox } from "@/components/ui/checkbox"; +import { motion, PanInfo, useAnimation } from 'framer-motion'; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { triggerConfetti } from "@/lib/confetti"; +import { playSuccessSound } from "@/lib/sounds"; +import { simulateAIDecomposition } from "@/lib/ai-simulator"; +import { Wand2, Loader2 } from "lucide-react"; interface TaskCardProps { task: Task; @@ -33,11 +39,32 @@ interface TaskCardProps { onEdit?: () => void; onDelete?: () => void; onStatusChange?: (status: Task['status']) => void; + onUpdate?: (updates: Partial) => void; isDragging?: boolean; } -export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, isDragging }: TaskCardProps) { +export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, onUpdate, isDragging }: TaskCardProps) { const { t } = useTranslation(); + const [isAnalyzing, setIsAnalyzing] = useState(false); + + const handleAIMagic = async (e: React.MouseEvent) => { + e.stopPropagation(); + if (isAnalyzing) return; + + setIsAnalyzing(true); + try { + const aiContent = await simulateAIDecomposition(task.title); + const newDescription = (task.description || '') + aiContent; + + onUpdate?.({ description: newDescription }); + triggerConfetti(0.5, 0.5); // Small burst for "Magic" + playSuccessSound(); + } catch (err) { + console.error(err); + } finally { + setIsAnalyzing(false); + } + }; // Fetch labels to get the label color const { data: labels = [] } = useQuery({ @@ -92,330 +119,369 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe if (task.status === 'done') { onStatusChange?.('todo'); } else { + triggerConfetti(e.clientX / window.innerWidth, e.clientY / window.innerHeight); + playSuccessSound(); onStatusChange?.('done'); } }; + const controls = useAnimation(); + + const handleDragEnd = async (event: any, info: PanInfo) => { + if (info.offset.x > 100) { + // Swiped right -> Complete + triggerConfetti(0.5, 0.5); + playSuccessSound(); + onStatusChange?.('done'); + await controls.start({ x: 500, opacity: 0 }); + } else if (info.offset.x < -100 && onDelete) { + // Swiped left -> Delete (optional, maybe just shake for now) + onDelete(); + } else { + controls.start({ x: 0 }); + } + }; + return ( - - - -
- {/* Checkbox for quick completion */} - - - - - -

{t('taskCard.toggleComplete')}

-
-
- -
{ - onEdit?.(); - console.log(`Task card clicked: ${task.title}`); - }} - > -

- {task.title} -

- {task.description && ( -

- {task.description} -

- )} - -
- - {t(`priority.${task.priority}`)} - - - - {t(`status.${task.status}`)} - + + + + +
+ {/* Checkbox for quick completion */} + + + + + +

{t('taskCard.toggleComplete')}

+
+
- {task.dueDate && ( -
- - - {task.dueDate.toLocaleDateString()} - -
- )} -
- - {(task.timeTracked > 0 || task.isTracking) && ( -
- - - {task.timeTracked > 0 ? formatTime(task.timeTracked) : '0m'} - {task.isTracking && ` (${t('taskCard.running')})`} - -
- )} -
- -
- {/* Quick delete button */} - {onDelete && ( - - - - - -

{t('taskCard.quickDelete')}

-
-
- )} - - - - - - e.stopPropagation()}> - { - e.stopPropagation(); +
{ onEdit?.(); - console.log(`Edit task: ${task.title}`); + console.log(`Task card clicked: ${task.title}`); }} - data-testid={`menu-edit-${task.id}`} > - - {t('taskCard.editTask')} - - - { - e.stopPropagation(); - handleToggleTimer(); - }} - data-testid={`menu-timer-${task.id}`} - > - - {task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')} - - - - - {task.status !== 'done' && ( - { - e.stopPropagation(); - onStatusChange?.('done'); - console.log(`Mark task as done: ${task.title}`); - }} - data-testid={`menu-complete-${task.id}`} - > - - {t('taskCard.markAsDone')} - - )} - - {task.status === 'todo' && ( - { - e.stopPropagation(); - onStatusChange?.('inProgress'); - console.log(`Start working on: ${task.title}`); - }} - data-testid={`menu-start-${task.id}`} - > - - {t('taskCard.startWorking')} - - )} - - {task.status === 'inProgress' && ( - { - e.stopPropagation(); - onStatusChange?.('todo'); - console.log(`Move back to todo: ${task.title}`); - }} - data-testid={`menu-todo-${task.id}`} - > - - {t('taskCard.moveToTodo')} - - )} - - {task.status === 'done' && ( - { - e.stopPropagation(); - onStatusChange?.('inProgress'); - console.log(`Reopen task: ${task.title}`); - }} - data-testid={`menu-reopen-${task.id}`} - > - - {t('taskCard.reopenTask')} - - )} - - - - {onDelete && ( - { - e.stopPropagation(); - onDelete(); - console.log(`Delete task: ${task.title}`); - }} - className="text-destructive" - data-testid={`menu-delete-${task.id}`} - > - - {t('taskCard.deleteTask')} - - )} - - -
-
- - - - { - e.stopPropagation(); - onEdit?.(); - }} - data-testid={`context-menu-edit-${task.id}`} - > - - {t('taskCard.editTask')} - - - { - e.stopPropagation(); - handleToggleTimer(); - }} - data-testid={`context-menu-timer-${task.id}`} - > - - {task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')} - - - - - {task.status !== 'done' && ( - + {task.title} + + {task.description && ( +

+ {task.description} +

+ )} + +
+ + {t(`priority.${task.priority}`)} + + + + {t(`status.${task.status}`)} + + + {task.dueDate && ( +
+ + + {task.dueDate.toLocaleDateString()} + +
+ )} + {task.energyLevel && task.energyLevel !== 'medium' && ( + + {t(`energy.${task.energyLevel}`)} + + )} + + {task.estimatedDuration && ( + + ⏳ {task.estimatedDuration}m + + )} +
+ + {(task.timeTracked > 0 || task.isTracking) && ( +
+ + + {task.timeTracked > 0 ? formatTime(task.timeTracked) : '0m'} + {task.isTracking && ` (${t('taskCard.running')})`} + +
+ )} +
+ +
+ {/* Quick delete button */} + {onDelete && ( + + + + + +

{t('taskCard.quickDelete')}

+
+
+ )} + + + + + + e.stopPropagation()}> + { + e.stopPropagation(); + onEdit?.(); + console.log(`Edit task: ${task.title}`); + }} + data-testid={`menu-edit-${task.id}`} + > + + {t('taskCard.editTask')} + + + { + e.stopPropagation(); + handleToggleTimer(); + }} + data-testid={`menu-timer-${task.id}`} + > + + {task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')} + + + + + {task.status !== 'done' && ( + { + e.stopPropagation(); + onStatusChange?.('done'); + console.log(`Mark task as done: ${task.title}`); + }} + data-testid={`menu-complete-${task.id}`} + > + + {t('taskCard.markAsDone')} + + )} + + {task.status === 'todo' && ( + { + e.stopPropagation(); + onStatusChange?.('inProgress'); + console.log(`Start working on: ${task.title}`); + }} + data-testid={`menu-start-${task.id}`} + > + + {t('taskCard.startWorking')} + + )} + + {task.status === 'inProgress' && ( + { + e.stopPropagation(); + onStatusChange?.('todo'); + console.log(`Move back to todo: ${task.title}`); + }} + data-testid={`menu-todo-${task.id}`} + > + + {t('taskCard.moveToTodo')} + + )} + + {task.status === 'done' && ( + { + e.stopPropagation(); + onStatusChange?.('inProgress'); + console.log(`Reopen task: ${task.title}`); + }} + data-testid={`menu-reopen-${task.id}`} + > + + {t('taskCard.reopenTask')} + + )} + + + + {onDelete && ( + { + e.stopPropagation(); + onDelete(); + console.log(`Delete task: ${task.title}`); + }} + className="text-destructive" + data-testid={`menu-delete-${task.id}`} + > + + {t('taskCard.deleteTask')} + + )} + + +
+
+
+
+ + { e.stopPropagation(); - onStatusChange?.('done'); + onEdit?.(); }} - data-testid={`context-menu-complete-${task.id}`} + data-testid={`context-menu-edit-${task.id}`} > - - {t('taskCard.markAsDone')} + + {t('taskCard.editTask')} - )} - - {task.status === 'todo' && ( - { e.stopPropagation(); - onStatusChange?.('inProgress'); + handleToggleTimer(); }} - data-testid={`context-menu-start-${task.id}`} + data-testid={`context-menu-timer-${task.id}`} > - - {t('taskCard.startWorking')} + + {task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')} - )} - - {task.status === 'inProgress' && ( - { - e.stopPropagation(); - onStatusChange?.('todo'); - }} - data-testid={`context-menu-todo-${task.id}`} - > - - {t('taskCard.moveToTodo')} - - )} - - {task.status === 'done' && ( - { - e.stopPropagation(); - onStatusChange?.('inProgress'); - }} - data-testid={`context-menu-reopen-${task.id}`} - > - - {t('taskCard.reopenTask')} - - )} - - - - {onDelete && ( - { - e.stopPropagation(); - onDelete(); - }} - className="text-destructive" - data-testid={`context-menu-delete-${task.id}`} - > - - {t('taskCard.deleteTask')} - - )} - -
+ + + + {task.status !== 'done' && ( + { + e.stopPropagation(); + onStatusChange?.('done'); + }} + data-testid={`context-menu-complete-${task.id}`} + > + + {t('taskCard.markAsDone')} + + )} + + {task.status === 'todo' && ( + { + e.stopPropagation(); + onStatusChange?.('inProgress'); + }} + data-testid={`context-menu-start-${task.id}`} + > + + {t('taskCard.startWorking')} + + )} + + {task.status === 'inProgress' && ( + { + e.stopPropagation(); + onStatusChange?.('todo'); + }} + data-testid={`context-menu-todo-${task.id}`} + > + + {t('taskCard.moveToTodo')} + + )} + + {task.status === 'done' && ( + { + e.stopPropagation(); + onStatusChange?.('inProgress'); + }} + data-testid={`context-menu-reopen-${task.id}`} + > + + {t('taskCard.reopenTask')} + + )} + + + + {onDelete && ( + { + e.stopPropagation(); + onDelete(); + }} + className="text-destructive" + data-testid={`context-menu-delete-${task.id}`} + > + + {t('taskCard.deleteTask')} + + )} + + + ); } \ No newline at end of file diff --git a/client/src/components/TaskCreationModal.tsx b/client/src/components/TaskCreationModal.tsx index 6cd4ad2..309f7c7 100644 --- a/client/src/components/TaskCreationModal.tsx +++ b/client/src/components/TaskCreationModal.tsx @@ -10,6 +10,8 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { CalendarIcon, Plus, Tag } from 'lucide-react'; import { Task, Label } from '@shared/schema'; import { useQuery } from '@tanstack/react-query'; +import { parseTaskInput } from '../lib/nlp'; +import { Sparkles } from 'lucide-react'; interface TaskCreationModalProps { isOpen: boolean; @@ -22,9 +24,13 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium'); + const [energyLevel, setEnergyLevel] = useState<'low' | 'medium' | 'high'>('medium'); + const [estimatedDuration, setEstimatedDuration] = useState(); const [dueDate, setDueDate] = useState(); const [labelId, setLabelId] = useState(); + const [isCalendarOpen, setIsCalendarOpen] = useState(false); + const [error, setError] = useState(null); // Fetch labels const { data: labels = [] } = useQuery({ @@ -32,28 +38,34 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat }); const handleSave = () => { - if (!title.trim()) return; - + if (!title.trim()) { + setError(t('taskCreation.titleRequired') || 'Title is required'); + return; + } + const newTask: Partial = { title: title.trim(), description: description.trim() || undefined, priority, + energyLevel, + estimatedDuration, dueDate, labelId, status: 'todo', timeTracked: 0, isTracking: false }; - + onSave(newTask); console.log('New task created:', newTask); - + // Reset form setTitle(''); setDescription(''); setPriority('medium'); setDueDate(undefined); setLabelId(undefined); + setError(null); onClose(); }; @@ -70,6 +82,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat setPriority('medium'); setDueDate(undefined); setLabelId(undefined); + setError(null); onClose(); }; @@ -82,20 +95,39 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat {t('taskCreation.title')} - +
setTitle(e.target.value)} + onChange={(e) => { + setTitle(e.target.value); + const parsed = parseTaskInput(e.target.value); + if (parsed.priority) setPriority(parsed.priority); + if (parsed.dueDate) setDueDate(parsed.dueDate); + if (parsed.labelName) { + const foundLabel = labels.find(l => l.name.toLowerCase() === parsed.labelName?.toLowerCase()); + if (foundLabel) setLabelId(foundLabel.id); + } + if (error) setError(null); + }} onKeyDown={handleKeyDown} className="text-base" data-testid="input-task-title" autoFocus /> +
+ {title.includes('!') || title.includes('#') || title.toLowerCase().includes('tomorrow') ? ( +
+ + Smart Input Active +
+ ) : null} + {error &&

{error}

} +
- +