diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/components/timer-controls.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/components/timer-controls.tsx index e063603e2..6235a6d37 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/components/timer-controls.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/components/timer-controls.tsx @@ -42,7 +42,7 @@ import { toast } from '@tuturuuu/ui/sonner'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs'; import { Textarea } from '@tuturuuu/ui/textarea'; import { cn } from '@tuturuuu/utils/format'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; interface ExtendedWorkspaceTask extends Partial { board_name?: string; @@ -100,6 +100,8 @@ interface TimerControlsProps { formatDuration: (seconds: number) => string; // eslint-disable-next-line no-unused-vars apiCall: (url: string, options?: RequestInit) => Promise; + isDraggingTask?: boolean; + onGoToTasksTab?: () => void; } export function TimerControls({ @@ -116,6 +118,8 @@ export function TimerControls({ formatTime, formatDuration, apiCall, + isDraggingTask = false, + onGoToTasksTab, }: TimerControlsProps) { const [isLoading, setIsLoading] = useState(false); const [newSessionTitle, setNewSessionTitle] = useState(''); @@ -128,6 +132,30 @@ export function TimerControls({ const [justCompleted, setJustCompleted] = useState(null); + // Drag and drop state + const [isDragOver, setIsDragOver] = useState(false); + const dragCounterRef = useRef(0); + + // Task search and filter state + const [taskSearchQuery, setTaskSearchQuery] = useState(''); + const [taskFilters, setTaskFilters] = useState({ + priority: 'all', + status: 'all', + board: 'all', + list: 'all', + }); + const [isTaskDropdownOpen, setIsTaskDropdownOpen] = useState(false); + const [isSearchMode, setIsSearchMode] = useState(false); + + // Dropdown positioning state + const [dropdownPosition, setDropdownPosition] = useState<'below' | 'above'>( + 'below' + ); + + // Refs for positioning + const dropdownContainerRef = useRef(null); + const dropdownContentRef = useRef(null); + // Task creation state const [boards, setBoards] = useState([]); const [showTaskCreation, setShowTaskCreation] = useState(false); @@ -173,10 +201,28 @@ export function TimerControls({ if (taskId && taskId !== 'none') { const selectedTask = tasks.find((t) => t.id === taskId); if (selectedTask) { + // Set task mode and populate fields (same as drag & drop) + setSessionMode('task'); setNewSessionTitle(`Working on: ${selectedTask.name}`); + setNewSessionDescription(selectedTask.description || ''); + + // Show success feedback (same as drag & drop) + toast.success(`Task "${selectedTask.name}" ready to track!`, { + description: + 'Click Start Timer to begin tracking time for this task.', + duration: 3000, + }); + + // Close dropdown and exit search mode + setIsTaskDropdownOpen(false); + setIsSearchMode(false); + setTaskSearchQuery(''); } } else { + // Reset when no task selected setNewSessionTitle(''); + setNewSessionDescription(''); + setIsSearchMode(true); } }; @@ -193,6 +239,9 @@ export function TimerControls({ // Reset any temporary states setSelectedCategoryId('none'); + setIsSearchMode(true); + setTaskSearchQuery(''); + setIsTaskDropdownOpen(false); // Provide helpful feedback if (previousMode !== mode) { @@ -469,6 +518,62 @@ export function TimerControls({ setSelectedTaskId(template.task_id || 'none'); }; + // Drag and drop handlers + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + dragCounterRef.current++; + setIsDragOver(true); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + // Keep the drag over state active + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + dragCounterRef.current--; + + // Only set isDragOver to false when counter reaches 0 + if (dragCounterRef.current === 0) { + setIsDragOver(false); + } + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + dragCounterRef.current = 0; + setIsDragOver(false); + + try { + const data = JSON.parse(e.dataTransfer.getData('application/json')); + if (data.type === 'task' && data.task) { + const task = data.task; + + // Set task mode and populate fields + setSessionMode('task'); + setSelectedTaskId(task.id); + setNewSessionTitle(`Working on: ${task.name}`); + setNewSessionDescription(task.description || ''); + + // Exit search mode and show selected task + setIsSearchMode(false); + setTaskSearchQuery(''); + setIsTaskDropdownOpen(false); + + // Show success feedback + toast.success(`Task "${task.name}" ready to track!`, { + description: + 'Click Start Timer to begin tracking time for this task.', + duration: 3000, + }); + } + } catch (error) { + console.error('Error handling dropped task:', error); + toast.error('Failed to process dropped task'); + } + }; + const getCategoryColor = (color: string) => { const colorMap: Record = { RED: 'bg-dynamic-red/80', @@ -489,9 +594,192 @@ export function TimerControls({ const selectedBoard = boards.find((board) => board.id === selectedBoardId); const availableLists = selectedBoard?.task_lists || []; + // Filter tasks based on search and filters + const getFilteredTasks = () => { + return tasks.filter((task) => { + const matchesSearch = task.name + ?.toLowerCase() + .includes(taskSearchQuery.toLowerCase()); + const matchesPriority = + taskFilters.priority === 'all' || + String(task.priority) === taskFilters.priority; + const matchesStatus = + taskFilters.status === 'all' || + (task.completed ? 'completed' : 'active') === taskFilters.status; + const matchesBoard = + taskFilters.board === 'all' || task.board_name === taskFilters.board; + const matchesList = + taskFilters.list === 'all' || task.list_name === taskFilters.list; + + return ( + matchesSearch && + matchesPriority && + matchesStatus && + matchesBoard && + matchesList + ); + }); + }; + + // Get unique boards and lists for filter options + const uniqueBoards = [ + ...new Set( + tasks + .map((task) => task.board_name) + .filter((name): name is string => Boolean(name)) + ), + ]; + const uniqueLists = [ + ...new Set( + tasks + .map((task) => task.list_name) + .filter((name): name is string => Boolean(name)) + ), + ]; + + // Calculate dropdown position + const calculateDropdownPosition = useCallback(() => { + if (!dropdownContainerRef.current) return; + + const container = dropdownContainerRef.current; + const rect = container.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const dropdownHeight = 400; // max-height of dropdown + const buffer = 20; // Buffer from viewport edges + + // Calculate available space + const spaceBelow = viewportHeight - rect.bottom - buffer; + const spaceAbove = rect.top - buffer; + + // Prefer below unless there's significantly more space above + if ( + spaceBelow >= Math.min(dropdownHeight, 200) || + spaceBelow >= spaceAbove + ) { + setDropdownPosition('below'); + } else { + setDropdownPosition('above'); + } + }, []); + + // Check if dropdown is visible in viewport + const isDropdownVisible = useCallback(() => { + if (!dropdownContainerRef.current) return true; + + const rect = dropdownContainerRef.current.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const viewportWidth = window.innerWidth; + + // Check if the container is still reasonably visible + const isVerticallyVisible = rect.bottom >= 0 && rect.top <= viewportHeight; + const isHorizontallyVisible = rect.right >= 0 && rect.left <= viewportWidth; + const hasMinimumVisibility = rect.height > 0 && rect.width > 0; + + return isVerticallyVisible && isHorizontallyVisible && hasMinimumVisibility; + }, []); + + // Open dropdown with position calculation + const openDropdown = useCallback(() => { + setIsTaskDropdownOpen(true); + // Use requestAnimationFrame to ensure DOM is ready for position calculation + requestAnimationFrame(() => { + calculateDropdownPosition(); + }); + }, [calculateDropdownPosition]); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + // Check if click is outside the dropdown container + if ( + !target.closest('[data-task-dropdown]') && + !target.closest('.absolute.z-\\[100\\]') + ) { + setIsTaskDropdownOpen(false); + setIsSearchMode(false); + } + }; + + if (isTaskDropdownOpen) { + // Use capture phase to ensure we catch the event before other handlers + document.addEventListener('mousedown', handleClickOutside, true); + return () => + document.removeEventListener('mousedown', handleClickOutside, true); + } + }, [isTaskDropdownOpen]); + + // Handle scroll and resize events + useEffect(() => { + let scrollTimeout: ReturnType; + + const handleScroll = () => { + if (isTaskDropdownOpen) { + // Clear previous timeout + clearTimeout(scrollTimeout); + + // Throttle scroll handling + scrollTimeout = setTimeout(() => { + if (!isDropdownVisible()) { + setIsTaskDropdownOpen(false); + setIsSearchMode(false); + } else { + calculateDropdownPosition(); + } + }, 16); // ~60fps + } + }; + + const handleResize = () => { + if (isTaskDropdownOpen) { + calculateDropdownPosition(); + } + }; + + if (isTaskDropdownOpen) { + // Calculate initial position + calculateDropdownPosition(); + + // Add event listeners + window.addEventListener('scroll', handleScroll, true); + window.addEventListener('resize', handleResize); + + return () => { + clearTimeout(scrollTimeout); + window.removeEventListener('scroll', handleScroll, true); + window.removeEventListener('resize', handleResize); + }; + } + }, [isTaskDropdownOpen, calculateDropdownPosition, isDropdownVisible]); + // Keyboard shortcuts useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { + // Don't trigger shortcuts when typing in input fields + const isInputFocused = + document.activeElement?.tagName === 'INPUT' || + document.activeElement?.tagName === 'TEXTAREA' || + document.activeElement?.getAttribute('contenteditable') === 'true'; + + // Escape to close dropdown or cancel drag + if (event.key === 'Escape') { + if (isTaskDropdownOpen) { + setIsTaskDropdownOpen(false); + return; + } + if (isDraggingTask) { + // Note: This won't actually cancel the drag since it's controlled by parent + // But it provides visual feedback + toast.info( + 'Press ESC while dragging to cancel (drag outside to cancel)' + ); + return; + } + } + + // Skip other shortcuts if typing in input + if (isInputFocused) return; + // Ctrl/Cmd + Enter to start/stop timer if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { event.preventDefault(); @@ -507,15 +795,92 @@ export function TimerControls({ event.preventDefault(); pauseTimer(); } + + // Ctrl/Cmd + T to open task dropdown + if ((event.ctrlKey || event.metaKey) && event.key === 't' && !isRunning) { + event.preventDefault(); + setIsTaskDropdownOpen(!isTaskDropdownOpen); + } + + // Ctrl/Cmd + M to switch between task/manual mode + if ((event.ctrlKey || event.metaKey) && event.key === 'm' && !isRunning) { + event.preventDefault(); + setSessionMode(sessionMode === 'task' ? 'manual' : 'task'); + toast.success( + `Switched to ${sessionMode === 'task' ? 'manual' : 'task'} mode` + ); + } + + // Arrow keys for task navigation when dropdown is open + if ( + isTaskDropdownOpen && + (event.key === 'ArrowDown' || event.key === 'ArrowUp') + ) { + event.preventDefault(); + const filteredTasks = getFilteredTasks(); + if (filteredTasks.length === 0) return; + + const currentIndex = filteredTasks.findIndex( + (task) => task.id === selectedTaskId + ); + let nextIndex; + + if (event.key === 'ArrowDown') { + nextIndex = + currentIndex < filteredTasks.length - 1 ? currentIndex + 1 : 0; + } else { + nextIndex = + currentIndex > 0 ? currentIndex - 1 : filteredTasks.length - 1; + } + + const nextTask = filteredTasks[nextIndex]; + if (nextTask?.id) { + setSelectedTaskId(nextTask.id); + } + } + + // Enter to select highlighted task when dropdown is open + if ( + isTaskDropdownOpen && + event.key === 'Enter' && + selectedTaskId !== 'none' + ) { + event.preventDefault(); + handleTaskSelectionChange(selectedTaskId); + } + + // Space to start timer with current selection + if (event.key === ' ' && !isRunning && !isInputFocused) { + event.preventDefault(); + if (newSessionTitle.trim()) { + startTimer(); + } + } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [isRunning, newSessionTitle, startTimer, stopTimer, pauseTimer]); + }, [ + isRunning, + newSessionTitle, + startTimer, + stopTimer, + pauseTimer, + isTaskDropdownOpen, + isDraggingTask, + selectedTaskId, + sessionMode, + ]); return ( <> - + @@ -530,6 +895,14 @@ export function TimerControls({ to start/stop ⌘/Ctrl + P to pause + ⌘/Ctrl + T + for tasks + ⌘/Ctrl + M + to switch mode + Space + to start + ↑↓ + to navigate @@ -633,10 +1006,61 @@ export function TimerControls({ ) : (
-
- -

- Ready to start tracking time +

+ +

+ {isDragOver + ? 'Drop task here to start tracking' + : isDraggingTask + ? 'Drag task here to start tracking' + : 'Ready to start tracking time'} +

+

+ {isDragOver + ? 'Release to select this task' + : isDraggingTask + ? 'Drop zone is ready • Drag outside to cancel' + : 'Drag tasks to the search field or select manually below'}

@@ -680,65 +1104,461 @@ export function TimerControls({ - + setTaskSearchQuery(e.target.value) + } + onFocus={() => { + setIsSearchMode(true); + openDropdown(); + }} + className={cn( + 'h-auto min-h-[2.5rem] pr-10 transition-all duration-200', + isDragOver + ? 'border-blue-500 bg-blue-50/50 dark:bg-blue-950/20' + : isDraggingTask + ? 'border-blue-400/60 bg-blue-50/20 dark:bg-blue-950/10' + : '' )} - {task.board_name && task.list_name && ( -
-
- - - {task.board_name} - -
-
- - - {task.list_name} - -
+ autoFocus={isSearchMode} + /> + +
+ )} + + {/* Dropdown Content */} + {isTaskDropdownOpen && ( +
{ + e.stopPropagation(); + }} + > + {/* Filter Buttons */} +
+
+ Quick Filters +
+
+ + {uniqueBoards.map((board) => ( + + ))} +
+ +
+ + {uniqueLists.map((list) => ( + + ))} +
+ + {(taskSearchQuery || + taskFilters.board !== 'all' || + taskFilters.list !== 'all') && ( +
+ + {getFilteredTasks().length} of{' '} + {tasks.length} tasks + +
)}
+ + {/* Task List */} +
+ {getFilteredTasks().length === 0 ? ( +
+ {taskSearchQuery || + taskFilters.board !== 'all' || + taskFilters.list !== 'all' ? ( + <> +
+ No tasks found matching your criteria +
+ + + ) : ( + 'No tasks available' + )} +
+ ) : ( + getFilteredTasks().map((task) => ( + + )) + )} +
- - ))} - - + )} +
- {(selectedTaskId === 'none' || !selectedTaskId) && ( -
-

- No task selected? We'll help you create one! -

-
+ {(selectedTaskId === 'none' || !selectedTaskId) && ( +
+

+ No task selected? We'll help you create one! +

+
+ )} + )}
diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/time-tracker-content.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/time-tracker-content.tsx index 4ba427b85..fa88bed20 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/time-tracker-content.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/time-tracker-content.tsx @@ -4,7 +4,6 @@ import { ActivityHeatmap } from './components/activity-heatmap'; import { CategoryManager } from './components/category-manager'; import { GoalManager } from './components/goal-manager'; import { SessionHistory } from './components/session-history'; -import { StatsOverview } from './components/stats-overview'; import { TimerControls } from './components/timer-controls'; import { UserSelector } from './components/user-selector'; import { useCurrentUser } from './hooks/use-current-user'; @@ -16,19 +15,38 @@ import type { } from '@tuturuuu/types/db'; import { Alert, AlertDescription } from '@tuturuuu/ui/alert'; import { Button } from '@tuturuuu/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@tuturuuu/ui/card'; import { AlertCircle, Calendar, + CheckCircle, + ChevronLeft, + ChevronRight, Clock, + Copy, + History, + MapPin, + Pause, RefreshCw, + RotateCcw, Settings, + Sparkles, + Tag, + Target, Timer, TrendingUp, WifiOff, + Zap, } from '@tuturuuu/ui/icons'; +import { Input } from '@tuturuuu/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@tuturuuu/ui/select'; import { toast } from '@tuturuuu/ui/sonner'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs'; +import { Tabs, TabsContent } from '@tuturuuu/ui/tabs'; import { cn } from '@tuturuuu/utils/format'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -370,7 +388,7 @@ export default function TimeTrackerContent({ clearInterval(refreshIntervalRef.current); } }; - }, [fetchData, isLoading, retryCount]); + }, [isLoading, retryCount]); // Remove fetchData dependency // Timer effect with better cleanup useEffect(() => { @@ -401,7 +419,7 @@ export default function TimeTrackerContent({ // Load data on mount and when dependencies change useEffect(() => { fetchData(); - }, [fetchData]); + }, [wsId, currentUserId, selectedUserId]); // Only depend on actual values, not the function // Online/offline detection useEffect(() => { @@ -420,7 +438,7 @@ export default function TimeTrackerContent({ window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; - }, [fetchData, retryCount]); + }, [retryCount]); // Remove fetchData dependency // Cleanup on unmount useEffect(() => { @@ -449,7 +467,39 @@ export default function TimeTrackerContent({ // Retry function with exponential backoff const handleRetry = useCallback(() => { fetchData(true, true); - }, [fetchData]); + }, []); // Remove fetchData dependency + + // Quick Actions Carousel + const [carouselView, setCarouselView] = useState(0); + const [lastUserInteraction, setLastUserInteraction] = useState(Date.now()); + + // Auto-advance carousel every 15 seconds (pauses when user interacts) + useEffect(() => { + const interval = setInterval(() => { + const timeSinceLastInteraction = Date.now() - lastUserInteraction; + if (timeSinceLastInteraction >= 15000) { + // 15 seconds + setCarouselView((prev) => (prev === 2 ? 0 : prev + 1)); + } + }, 15000); + + return () => clearInterval(interval); + }, [lastUserInteraction]); + + // Sidebar View Switching + const [sidebarView, setSidebarView] = useState< + 'analytics' | 'tasks' | 'reports' | 'settings' + >('analytics'); + + // Drag and drop state for highlighting drop zones + const [isDraggingTask, setIsDraggingTask] = useState(false); + + // Tasks sidebar search and filter state + const [tasksSidebarSearch, setTasksSidebarSearch] = useState(''); + const [tasksSidebarFilters, setTasksSidebarFilters] = useState({ + board: 'all', + list: 'all', + }); if (isLoadingUser || !currentUserId) { return ( @@ -471,312 +521,1287 @@ export default function TimeTrackerContent({ isLoading && 'opacity-50' )} > - {/* Header with User Selector */} -
-
-

- Time Tracker -

-

- {isViewingOtherUser - ? "Viewing another user's time tracking data" - : 'Track and manage your time across projects'} -

- {!isViewingOtherUser && ( -

- Week starts Monday • Times updated in real-time - {(() => { - const today = new Date(); - const dayOfWeek = today.getDay(); - - if (dayOfWeek === 1) { - return ' • Week resets today! 🎯'; - } else if (dayOfWeek === 0) { - return ' • Week resets tomorrow'; - } else { - return ' • Week resets Monday'; - } - })()} -

- )} - {lastRefresh && ( -

- Last updated: {lastRefresh.toLocaleTimeString()} - {isOffline && ( - - - Offline - - )} -

- )} -
-
- - -
-
- - {/* Error Alert with better UX */} - {error && ( - - - -
- - {isOffline - ? 'You are offline. Some features may not work.' - : error} - - {retryCount > 0 && ( -

- Retried {retryCount} time{retryCount > 1 ? 's' : ''} + {/* Enhanced Header with Quick Stats */} +

+ {/* Main Header Section */} +
+
+
+
+ +
+
+

+ Time Tracker +

+

+ {isViewingOtherUser + ? "Viewing another user's time tracking data" + : 'Track and manage your time across projects'}

- )} +
+ + {!isViewingOtherUser && ( +
+
+
+ Week starts Monday +
+ +
+
+ Times updated in real-time +
+ {(() => { + const today = new Date(); + const dayOfWeek = today.getDay(); + + if (dayOfWeek === 1) { + return ( + <> + +
+ 🎯 + Week resets today! +
+ + ); + } else if (dayOfWeek === 0) { + return ( + <> + + Week resets tomorrow + + ); + } else { + return ( + <> + + Week resets Monday + + ); + } + })()} +
+ )} + + {lastRefresh && ( +
+ + Last updated: {lastRefresh.toLocaleTimeString()} + {isOffline && ( +
+ + Offline +
+ )} +
+ )} +
+ +
- - - )} + +
+
- + {/* Quick Actions Carousel */} + {!isViewingOtherUser && ( +
+ {/* Carousel Navigation */} +
+
+ - {timerStats.dailyActivity && ( - - )} +
+ {[0, 1, 2].map((index) => ( +
- {/* Main Content Tabs with improved mobile design */} - - - {!isViewingOtherUser && ( - - - Timer - - )} - - - History - - {!isViewingOtherUser && ( - - - Categories - - )} - - - Goals - - + +
- {!isViewingOtherUser && ( - + {carouselView === 0 && 'Smart Quick Actions'} + {carouselView === 1 && 'Context-Aware Dashboard'} + {carouselView === 2 && 'Productivity Command Center'} +
+
+ + {/* Carousel Content */} +
+
+ {/* View 0: Smart Quick Actions */} +
+
+ {/* Continue Last Session */} + + + {/* Start Most Used Task */} + + + {/* Quick 25min Focus */} + + + {/* From Template */} + +
+
+ + {/* View 1: Context-Aware Dashboard */} +
+
+ {/* Today's Calendar */} + + + {/* Suggested Tasks */} + + + {/* Goal Progress */} + + + {/* Quick Actions */} + +
+
+ + {/* View 2: Productivity Command Center */} +
+
+ {/* Active Tasks */} + + + {/* Focus Score */} + + + {/* Break Timer */} + + + {/* Session History */} + +
+
+
+
+
+ )} + + {/* Current Session Status Banner */} + {!isViewingOtherUser && currentSession && ( +
+
+
+
+
+
+
+

+ Currently tracking: +

+ + {currentSession.title} + +
+

+ Started at{' '} + {new Date(currentSession.start_time).toLocaleTimeString()} • + Running for {formatTime(elapsedTime)} +

+
+
+ {formatTime(elapsedTime)} +
+
+
+ )} + + {/* Error Alert with better UX */} + {error && ( + -
-
- fetchData(false)} - formatTime={formatTime} - formatDuration={formatDuration} - apiCall={apiCall} - /> + + +
+ + {isOffline + ? 'You are offline. Some features may not work.' + : error} + + {retryCount > 0 && ( +

+ Retried {retryCount} time{retryCount > 1 ? 's' : ''} +

+ )}
-
- - - - - Recent Sessions - - - -
- {recentSessions.slice(0, 5).map((session, index) => ( -
+ + + + )} + + {/* New Layout: Analytics sidebar on left, Timer controls and tabs on right */} +
+ {/* Right Side: Tabs with Timer Controls - First on mobile */} +
+
+ {/* Tab Navigation - Styled like sidebar switcher */} +
+
+ {!isViewingOtherUser && ( + + )} + + {!isViewingOtherUser && ( + + )} + +
+
+ + {/* Main Tabs - Timer, History, Categories, Goals */} + + {/* Tab Content */} + {!isViewingOtherUser && ( + +
+ fetchData(false)} + formatTime={formatTime} + formatDuration={formatDuration} + apiCall={apiCall} + isDraggingTask={isDraggingTask} + onGoToTasksTab={() => { + setSidebarView('tasks'); + toast.success( + 'Switched to Tasks tab - create your first task!' + ); + }} + /> +
+
+ )} + + + {isViewingOtherUser && ( +
+

+ + You're viewing another user's session history. You can + see their sessions but cannot edit them. +

+
+ )} + fetchData(false)} + readOnly={isViewingOtherUser} + formatDuration={formatDuration} + apiCall={apiCall} + /> +
+ + {!isViewingOtherUser && ( + + fetchData(false)} + readOnly={isViewingOtherUser} + apiCall={apiCall} + /> + + )} + + + {isViewingOtherUser && ( +
+

+ + You're viewing another user's goals. You can see their + progress but cannot edit their goals. +

+
+ )} + fetchData(false)} + readOnly={isViewingOtherUser} + formatDuration={formatDuration} + apiCall={apiCall} + /> +
+
+
+
+ + {/* Left Side: Switchable Sidebar Views - Second on mobile */} +
+
+ {/* Sidebar View Switcher */} +
+
+ + + + +
+
+ + {/* Sidebar Content */} + {sidebarView === 'analytics' && ( + <> + {/* Stats Overview - Enhanced for sidebar */} +
+
+
+
+ +
+
+

+ Your Progress +

+

+ Track your productivity metrics ⚡ +

+
+
+
+ {/* Custom sidebar-optimized stats layout */} +
+ {/* Today */} +
+
+
+ +
-

- {session.title} +

+

+ Today +

+ + {new Date().getDay() === 0 || + new Date().getDay() === 6 + ? '🏖️' + : '💼'} + +
+

+ {new Date().toLocaleDateString('en-US', { + weekday: 'long', + })}

-
- {session.category && ( - -
- {session.category.name} -
- )} - - - {new Date( - session.start_time - ).toLocaleDateString()} +

+ {formatDuration(timerStats.todayTime)} +

+
+
+
+ + {/* This Week */} +
+
+
+ +
+
+
+

+ This Week +

+ 📊 +
+

+ {(() => { + const today = new Date(); + const dayOfWeek = today.getDay(); + const daysToSubtract = + dayOfWeek === 0 ? 6 : dayOfWeek - 1; + const startOfWeek = new Date(today); + startOfWeek.setDate( + today.getDate() - daysToSubtract + ); + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 6); + return `${startOfWeek.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${endOfWeek.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`; + })()} +

+

+ {formatDuration(timerStats.weekTime)} +

+
+
+
+ + {/* This Month */} +
+
+
+ +
+
+
+

+ This Month +

+ 🚀 +
+

+ {new Date().toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + })} +

+

+ {formatDuration(timerStats.monthTime)} +

+
+
+
+ + {/* Streak */} +
+
+
+ +
+
+
+

+ Streak +

+ + {timerStats.streak >= 7 ? '🏆' : '⭐'}
+

+ {timerStats.streak > 0 + ? 'consecutive days' + : 'start today!'} +

+

+ {timerStats.streak} days +

-
-

- {session.duration_seconds - ? formatDuration(session.duration_seconds) - : '-'} +

+
+
+
+ + {/* Activity Heatmap - Enhanced with better header */} + {timerStats.dailyActivity && ( +
+
+
+
+ +
+
+

+ Activity Heatmap +

+

+ {(() => { + const totalDuration = + timerStats.dailyActivity?.reduce( + (sum, day) => sum + day.duration, + 0 + ) || 0; + return totalDuration > 0 + ? `${formatDuration(totalDuration)} tracked this year 🔥` + : 'Start tracking to see your activity pattern 🌱'; + })()}

- ))} - {recentSessions.length === 0 && ( -
- -

- No sessions yet. Start your first timer! +

+ {/* Remove the original header from ActivityHeatmap component */} +
+ +
+
+ )} + + )} + + {/* Tasks View */} + {sidebarView === 'tasks' && ( +
+ {/* Tasks Header */} +
+ {/* Header Section */} +
+
+
+ +
+
+

+ Task Workspace +

+

+ Drag tasks to timer to start tracking 🎯

- )} +
- - -
-
- - )} - - {isViewingOtherUser && ( -
-

- - You're viewing another user's session history. You can see their - sessions but cannot edit them. -

-
- )} - fetchData(false)} - readOnly={isViewingOtherUser} - formatDuration={formatDuration} - apiCall={apiCall} - /> -
- - - {isViewingOtherUser && ( -
-

- - You're viewing another user's categories. You can see their - categories but cannot edit them. -

-
- )} - fetchData(false)} - readOnly={isViewingOtherUser} - apiCall={apiCall} - /> -
- - - {isViewingOtherUser && ( -
-

- - You're viewing another user's goals. You can see their progress - but cannot edit their goals. -

+ {/* Search and Filter Bar */} +
+
+
+ + setTasksSidebarSearch(e.target.value) + } + className="h-8 text-xs" + /> +
+ + +
+
+ + {/* Task List with Scrollable Container */} +
+ {(() => { + // Filter tasks for sidebar + const filteredSidebarTasks = tasks.filter((task) => { + if ( + tasksSidebarSearch && + !task.name + ?.toLowerCase() + .includes(tasksSidebarSearch.toLowerCase()) + ) { + return false; + } + if ( + tasksSidebarFilters.board && + tasksSidebarFilters.board !== 'all' && + task.board_name !== tasksSidebarFilters.board + ) { + return false; + } + if ( + tasksSidebarFilters.list && + tasksSidebarFilters.list !== 'all' && + task.list_name !== tasksSidebarFilters.list + ) { + return false; + } + return true; + }); + + if (tasks.length === 0) { + return ( +
+ +

+ No tasks available. Create tasks in your project + boards to see them here. +

+
+ ); + } + + if (filteredSidebarTasks.length === 0) { + return ( +
+ +

+ No tasks found matching your criteria. +

+
+ ); + } + + return ( + <> + {/* Task Count Header */} +
+ + {filteredSidebarTasks.length} task + {filteredSidebarTasks.length !== 1 + ? 's' + : ''}{' '} + available + {(tasksSidebarSearch || + (tasksSidebarFilters.board && + tasksSidebarFilters.board !== 'all') || + (tasksSidebarFilters.list && + tasksSidebarFilters.list !== 'all')) && + ` (filtered from ${tasks.length} total)`} + + + Drag to timer → + +
+ + {/* Scrollable Task Container */} +
+
+ {filteredSidebarTasks.map((task) => ( +
{ + e.dataTransfer.setData( + 'application/json', + JSON.stringify({ + type: 'task', + task: task, + }) + ); + setIsDraggingTask(true); + }} + onDragEnd={() => { + setIsDraggingTask(false); + }} + > +
+
+ +
+
+

+ {task.name} +

+ {task.description && ( +

+ {task.description} +

+ )} + {task.board_name && task.list_name && ( +
+
+ + + {task.board_name} + +
+
+ + + {task.list_name} + +
+
+ )} +
+
+ + Drag + + + + +
+
+
+ ))} +
+ + {/* Scroll indicator */} + {filteredSidebarTasks.length > 5 && ( +
+
+ Scroll for more + + + +
+
+ )} +
+ + ); + })()} +
+
+
+ )} + + {/* Reports View */} + {sidebarView === 'reports' && ( +
+
+
+
+
+ +
+
+

+ Reports & Analytics +

+

+ Detailed insights coming soon 📊 +

+
+
+
+ +
+ +

+ Advanced reporting features are coming soon. Stay tuned + for detailed analytics, custom reports, and productivity + insights. +

+
+
+
+ )} + + {/* Settings View */} + {sidebarView === 'settings' && ( +
+
+
+
+
+ +
+
+

+ Timer Settings +

+

+ Customize your tracking experience ⚙️ +

+
+
+
+ +
+ +

+ Timer settings and preferences will be available here. + Configure notifications, default categories, and more. +

+
+
+
+ )}
- )} - fetchData(false)} - readOnly={isViewingOtherUser} - formatDuration={formatDuration} - apiCall={apiCall} - /> - - +
+
+
); } diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx index 8286a7873..aea122864 100644 --- a/apps/web/src/app/[locale]/layout.tsx +++ b/apps/web/src/app/[locale]/layout.tsx @@ -119,7 +119,7 @@ export default async function RootLayout({ children, params }: Props) { diff --git a/bun.lock b/bun.lock index d446cd175..a26e688c7 100644 --- a/bun.lock +++ b/bun.lock @@ -721,7 +721,7 @@ "gsap": "^3.13.0", "input-otp": "^1.4.2", "lodash": "^4.17.21", - "lucide-react": "^0.514.0", + "lucide-react": "^0.515.0", "moment": "^2.30.1", "next": "15.3.3", "next-themes": "^0.4.6", @@ -3024,7 +3024,7 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.514.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HXD0OAMd+JM2xCjlwG1EGW9Nuab64dhjO3+MvdyD+pSUeOTBaVAPhQblKIYmmX4RyBYbdzW0VWnJpjJmxWGr6w=="], + "lucide-react": ["lucide-react@0.515.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Sy7bY0MeicRm2pzrnoHm2h6C1iVoeHyBU2fjdQDsXGP51fhkhau1/ZV/dzrcxEmAKsxYb6bGaIsMnGHuQ5s0dw=="], "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], diff --git a/packages/ui/src/components/ui/custom/structure.tsx b/packages/ui/src/components/ui/custom/structure.tsx index 17d9f9532..ae7c05d48 100644 --- a/packages/ui/src/components/ui/custom/structure.tsx +++ b/packages/ui/src/components/ui/custom/structure.tsx @@ -141,10 +141,12 @@ export function Structure({ )} + {/* Main content area - overflow-y-auto removed to prevent double scrollbars */} + {/* Body element now handles page-level scrolling */}