diff --git a/.replit b/.replit index f009312..a572720 100644 --- a/.replit +++ b/.replit @@ -14,10 +14,6 @@ run = ["npm", "run", "start"] localPort = 5000 externalPort = 80 -[[ports]] -localPort = 33679 -externalPort = 4200 - [[ports]] localPort = 35345 externalPort = 3002 diff --git a/client/src/App.tsx b/client/src/App.tsx index 3a12094..ec7ea25 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -304,6 +304,11 @@ function App() { onTaskDrop={handleTaskDrop} onTaskClick={handleTaskClick} onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())} + onTaskUpdate={handleTaskUpdate} + onTaskEdit={handleTaskClick} + onTaskDelete={handleTaskDelete} + onStartTimer={startTimer} + onStopTimer={stopTimer} /> ); diff --git a/client/src/components/CalendarView.tsx b/client/src/components/CalendarView.tsx index dc9c739..dcaeee3 100644 --- a/client/src/components/CalendarView.tsx +++ b/client/src/components/CalendarView.tsx @@ -1,25 +1,48 @@ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Calendar, Edit, Timer, CheckCircle, Play, Clock, Trash2 } from 'lucide-react'; import { Task, Label } from '@shared/schema'; import { useQuery } from '@tanstack/react-query'; import { addDays, format, isSameDay, startOfWeek, subWeeks } from 'date-fns'; import { de, enUS } from 'date-fns/locale'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; interface CalendarViewProps { tasks: Task[]; onTaskDrop?: (taskId: string, newDate: Date) => void; onTaskClick?: (task: Task) => void; onDateSelect?: (date: Date) => void; + onTaskUpdate?: (taskId: string, updates: Partial) => void; + onTaskEdit?: (task: Task) => void; + onTaskDelete?: (taskId: string) => void; + onStartTimer?: (taskId: string) => void; + onStopTimer?: (taskId: string) => void; } -export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSelect }: CalendarViewProps) { +export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSelect, onTaskUpdate, onTaskEdit, onTaskDelete, onStartTimer, onStopTimer }: CalendarViewProps) { const { t, i18n } = useTranslation(); const [currentDate, setCurrentDate] = useState(new Date()); const [draggedTask, setDraggedTask] = useState(null); + const [longPressTask, setLongPressTask] = useState(null); + const longPressTimerRef = useRef(null); + const [dropdownOpen, setDropdownOpen] = useState(false); + const longPressTriggered = useRef(false); // Get date-fns locale based on current language const dateLocale = i18n.language === 'de' ? de : enUS; @@ -69,6 +92,158 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel console.log(`Navigating to week of ${newDate.toLocaleDateString()}`); }; + // Long press handlers for mobile + const handleTouchStart = (task: Task) => { + // Clear any existing timeout + 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; + }, 300); + }, 500); // 500ms long press threshold + }; + + const handleTouchEnd = () => { + if (longPressTimerRef.current) { + clearTimeout(longPressTimerRef.current); + longPressTimerRef.current = null; + } + }; + + 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); + }; + + // Menu content component to avoid duplication + const TaskMenuContent = ({ task, closeMenu }: { task: Task; closeMenu?: () => void }) => ( + <> + { + e.stopPropagation(); + onTaskEdit?.(task); + closeMenu?.(); + }} + data-testid={`context-menu-edit-${task.id}`} + > + + {t('taskCard.editTask')} + + + { + e.stopPropagation(); + if (task.isTracking) { + onStopTimer?.(task.id); + } else { + onStartTimer?.(task.id); + } + closeMenu?.(); + }} + data-testid={`context-menu-timer-${task.id}`} + > + + {task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')} + + + + + {task.status !== 'done' && ( + { + e.stopPropagation(); + onTaskUpdate?.(task.id, { status: 'done' }); + closeMenu?.(); + }} + data-testid={`context-menu-complete-${task.id}`} + > + + {t('taskCard.markAsDone')} + + )} + + {task.status === 'todo' && ( + { + e.stopPropagation(); + onTaskUpdate?.(task.id, { status: 'inProgress' }); + closeMenu?.(); + }} + data-testid={`context-menu-start-${task.id}`} + > + + {t('taskCard.startWorking')} + + )} + + {task.status === 'inProgress' && ( + { + e.stopPropagation(); + onTaskUpdate?.(task.id, { status: 'todo' }); + closeMenu?.(); + }} + data-testid={`context-menu-todo-${task.id}`} + > + + {t('taskCard.moveToTodo')} + + )} + + {task.status === 'done' && ( + { + e.stopPropagation(); + onTaskUpdate?.(task.id, { status: 'inProgress' }); + closeMenu?.(); + }} + data-testid={`context-menu-reopen-${task.id}`} + > + + {t('taskCard.reopenTask')} + + )} + + + + {onTaskDelete && ( + { + e.stopPropagation(); + onTaskDelete(task.id); + closeMenu?.(); + }} + className="text-destructive" + data-testid={`context-menu-delete-${task.id}`} + > + + {t('taskCard.deleteTask')} + + )} + + ); + return (
{/* Header */} @@ -150,35 +325,38 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel const taskLabel = task.labelId && labels.length > 0 ? labels.find(label => label.id === task.labelId) : null; return ( -
handleDragStart(task.id)} - onDragEnd={handleDragEnd} - onClick={(e) => { - // Prevent click when dragging - if (draggedTask === task.id) return; - e.stopPropagation(); - onTaskClick?.(task); - console.log('Calendar task clicked:', task.title); - }} - className={`cursor-pointer hover-elevate ${draggedTask === task.id ? 'opacity-50' : ''}`} - data-testid={`calendar-task-${task.id}`} - > - -
- {task.title} + + +
handleDragStart(task.id)} + onDragEnd={handleDragEnd} + onTouchStart={() => handleTouchStart(task)} + onTouchEnd={handleTouchEnd} + onTouchMove={handleTouchEnd} + onClick={(e) => handleTaskClick(task, e)} + className={`cursor-pointer hover-elevate ${draggedTask === task.id ? 'opacity-50' : ''}`} + data-testid={`calendar-task-${task.id}`} + > + +
+ {task.title} +
+
- -
+ + + + + ); })} @@ -208,6 +386,125 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel {t('calendar.previousWeek')}
+ + {/* Mobile long-press fallback menu */} + {longPressTask && ( + { + setDropdownOpen(open); + if (!open) { + setLongPressTask(null); + } + }} + > + + + { + e.stopPropagation(); + onTaskEdit?.(longPressTask); + setDropdownOpen(false); + }} + data-testid={`dropdown-menu-edit-${longPressTask.id}`} + > + + {t('taskCard.editTask')} + + + { + e.stopPropagation(); + if (longPressTask.isTracking) { + onStopTimer?.(longPressTask.id); + } else { + onStartTimer?.(longPressTask.id); + } + setDropdownOpen(false); + }} + data-testid={`dropdown-menu-timer-${longPressTask.id}`} + > + + {longPressTask.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')} + + + + + {longPressTask.status !== 'done' && ( + { + e.stopPropagation(); + onTaskUpdate?.(longPressTask.id, { status: 'done' }); + setDropdownOpen(false); + }} + data-testid={`dropdown-menu-complete-${longPressTask.id}`} + > + + {t('taskCard.markAsDone')} + + )} + + {longPressTask.status === 'todo' && ( + { + e.stopPropagation(); + onTaskUpdate?.(longPressTask.id, { status: 'inProgress' }); + setDropdownOpen(false); + }} + data-testid={`dropdown-menu-start-${longPressTask.id}`} + > + + {t('taskCard.startWorking')} + + )} + + {longPressTask.status === 'inProgress' && ( + { + e.stopPropagation(); + onTaskUpdate?.(longPressTask.id, { status: 'todo' }); + setDropdownOpen(false); + }} + data-testid={`dropdown-menu-todo-${longPressTask.id}`} + > + + {t('taskCard.moveToTodo')} + + )} + + {longPressTask.status === 'done' && ( + { + e.stopPropagation(); + onTaskUpdate?.(longPressTask.id, { status: 'inProgress' }); + setDropdownOpen(false); + }} + data-testid={`dropdown-menu-reopen-${longPressTask.id}`} + > + + {t('taskCard.reopenTask')} + + )} + + + + {onTaskDelete && ( + { + e.stopPropagation(); + onTaskDelete(longPressTask.id); + setDropdownOpen(false); + }} + className="text-destructive" + data-testid={`dropdown-menu-delete-${longPressTask.id}`} + > + + {t('taskCard.deleteTask')} + + )} + + + )} ); } \ No newline at end of file