From 84d5529462978c9ba8173834b6d3e9add1e88e2d Mon Sep 17 00:00:00 2001 From: Adinorio Date: Sun, 15 Jun 2025 17:56:57 +0800 Subject: [PATCH 1/5] feat(time-tracker): new UI layout for better UX --- .../components/timer-controls.tsx | 75 +- .../time-tracker/time-tracker-content.tsx | 1264 +++++++++++++---- 2 files changed, 1054 insertions(+), 285 deletions(-) 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..5d7726887 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 @@ -128,6 +128,9 @@ export function TimerControls({ const [justCompleted, setJustCompleted] = useState(null); + // Drag and drop state + const [isDragOver, setIsDragOver] = useState(false); + // Task creation state const [boards, setBoards] = useState([]); const [showTaskCreation, setShowTaskCreation] = useState(false); @@ -469,6 +472,44 @@ export function TimerControls({ setSelectedTaskId(template.task_id || 'none'); }; + // Drag and drop handlers + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + 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 || ''); + + // 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', @@ -515,7 +556,15 @@ export function TimerControls({ return ( <> - + @@ -633,11 +682,27 @@ export function TimerControls({ ) : (
-
- -

- Ready to start tracking time +

+ +

+ {isDragOver ? "Drop task here to start tracking" : "Ready to start tracking time"}

+ {!isDragOver && ( +

+ Drag tasks from the sidebar or select manually below +

+ )}
{/* Session Mode Toggle */} 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..d5a418cdd 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 @@ -21,11 +21,21 @@ import { AlertCircle, Calendar, Clock, - RefreshCw, - Settings, - Timer, + ChevronLeft, + ChevronRight, + Copy, + CheckCircle, + RotateCcw, + Sparkles, + Target, TrendingUp, WifiOff, + Zap, + Pause, + History, + Settings, + Timer, + RefreshCw, } from '@tuturuuu/ui/icons'; import { toast } from '@tuturuuu/ui/sonner'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs'; @@ -91,7 +101,7 @@ export default function TimeTrackerContent({ initialData, }: TimeTrackerContentProps) { const { userId: currentUserId, isLoading: isLoadingUser } = useCurrentUser(); - const [activeTab, setActiveTab] = useState('timer'); + const [activeTab, setActiveTab] = useState('history'); const [selectedUserId, setSelectedUserId] = useState(null); // Use React Query for running session to sync with command palette @@ -451,6 +461,25 @@ export default function TimeTrackerContent({ fetchData(true, true); }, [fetchData]); + // 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'); + if (isLoadingUser || !currentUserId) { return (
@@ -471,162 +500,864 @@ 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 */} +
+
+ + +
+ {[0, 1, 2].map((index) => ( +
+ + +
+ +
+ {carouselView === 0 && "Smart Quick Actions"} + {carouselView === 1 && "Context-Aware Dashboard"} + {carouselView === 2 && "Productivity Command Center"} +
+
- {timerStats.dailyActivity && ( - - )} + {/* Carousel Content */} +
+
+ {/* View 0: Smart Quick Actions */} +
+
+ {/* Continue Last Session */} + - {/* Main Content Tabs with improved mobile design */} - - - {!isViewingOtherUser && ( - - - Timer - - )} - - - History - - {!isViewingOtherUser && ( - - - Categories - - )} - - - Goals - - + {/* Start Most Used Task */} + - {!isViewingOtherUser && ( - { + // TODO: Implement quick focus timer + toast.info("Quick 25min focus - Coming soon!"); + }} + className="group rounded-lg border border-purple-200/60 bg-gradient-to-br from-purple-50 to-purple-100/50 p-3 text-left transition-all duration-300 hover:shadow-md hover:scale-105 dark:border-purple-800/60 dark:from-purple-950/30 dark:to-purple-900/20" + > +
+
+ +
+
+

Quick Focus

+

+ 25 minutes +

+
+ 🎯 +
+ + + {/* 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 && ( + -
-
+ + +
+ + {isOffline + ? 'You are offline. Some features may not work.' + : error} + + {retryCount > 0 && ( +

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

+ )} +
+ +
+ + )} + + {/* New Layout: Analytics sidebar on left, Timer controls and tabs on right */} +
+ {/* Left Side: Switchable Sidebar Views */} +
+ {/* Sidebar View Switcher */} +
+
+ + + + +
+
+ + {/* Sidebar Content */} + {sidebarView === 'analytics' && ( + <> + {/* Stats Overview - Enhanced for sidebar */} +
+
+
+
+ +
+
+

+ Your Progress +

+

+ Track your productivity metrics ⚡ +

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

Today

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

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

+

{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

+
+
+
+
+
+ + {/* 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 🌱'; + })()} +

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

+ Task Workspace +

+

+ Drag tasks to timer to start tracking 🎯 +

+
+
+
+ + {/* Task List */} +
+ {tasks.length === 0 ? ( +
+ +

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

+
+ ) : ( + tasks.slice(0, 10).map((task) => ( +
{ + e.dataTransfer.setData('application/json', JSON.stringify({ + type: 'task', + task: task + })); + }} + > +
+
+ +
+
+

+ {task.name} +

+ {task.description && ( +

+ {task.description} +

+ )} + {task.board_name && task.list_name && ( +
+
+ + {task.board_name} + +
+
+ + {task.list_name} + +
+
+ )} +
+
+ Drag to timer +
+
+
+ )) + )} + + {tasks.length > 10 && ( +
+

+ Showing 10 of {tasks.length} tasks +

+
+ )} +
+
+
+ )} + + {/* 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. +

+
+
+
+ )} +
+ + {/* Right Side: Timer Controls and Tabs */} +
+ {/* Timer Controls - Always visible for current user */} + {!isViewingOtherUser && ( +
-
- - - - - Recent Sessions - - - -
- {recentSessions.slice(0, 5).map((session, index) => ( -
-
-

- {session.title} -

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

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

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

- No sessions yet. Start your first timer! -

-
- )} -
-
-
-
-
- - )} + )} - - {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. -

-
- )} - fetchData(false)} - readOnly={isViewingOtherUser} - formatDuration={formatDuration} - apiCall={apiCall} - /> -
- + {/* Tabs for additional functionality */} + + + + + History + + {!isViewingOtherUser && ( + + + Categories + + )} + + + Goals + + + + + {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} + /> +
+
+
+
+
); } From 74f12ba31a156cdf34715149622d3d9f1357a442 Mon Sep 17 00:00:00 2001 From: Adinorio Date: Mon, 16 Jun 2025 02:02:54 +0800 Subject: [PATCH 2/5] feat(time-tracker): better navigation options and overall functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement mobile-first layout and comprehensive keyboard accessibility ## Mobile Layout Optimization - Reorder grid layout to prioritize timer controls first on mobile devices - Use CSS order classes (order-1 lg:order-2) for responsive priority switching - Maintain desktop layout with analytics sidebar (2/5) and timer controls (3/5) ## Comprehensive Keyboard Accessibility ### Core Timer Controls - ⌘/Ctrl + Enter: Start/stop timer with current selection - ⌘/Ctrl + P: Pause active timer session - Space: Quick start timer (when not typing in inputs) - Escape: Close dropdowns or show drag cancellation hints ### Task Management Shortcuts - ⌘/Ctrl + T: Toggle task selection dropdown - ⌘/Ctrl + M: Switch between task/manual session modes - ↑/↓ Arrow Keys: Navigate through filtered tasks in dropdown - Enter: Select currently highlighted task from dropdown ### Smart Input Detection - Context-aware shortcut handling that disables when typing - Prevents interference with normal text input - Maintains accessibility for screen readers ## UI/UX Improvements - Fixed double scrollbar issue by optimizing scroll container hierarchy - Changed body overflow from 'scroll' to 'auto' for better UX - Removed redundant overflow-y-auto from main content area - Enhanced visual feedback with keyboard shortcut hints Files changed: - apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/time-tracker-content.tsx - apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/components/timer-controls.tsx - apps/web/src/app/[locale]/layout.tsx (scrollbar fix) - packages/ui/src/components/ui/custom/structure.tsx (scrollbar fix) --- .../components/timer-controls.tsx | 775 ++++++++++++++-- .../time-tracker/time-tracker-content.tsx | 833 +++++++++++------- apps/web/src/app/[locale]/layout.tsx | 2 +- .../ui/src/components/ui/custom/structure.tsx | 2 +- 4 files changed, 1203 insertions(+), 409 deletions(-) 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 5d7726887..a3f3d5ff9 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(''); @@ -130,6 +134,25 @@ export function TimerControls({ // 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([]); @@ -176,10 +199,27 @@ 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); } }; @@ -196,6 +236,9 @@ export function TimerControls({ // Reset any temporary states setSelectedCategoryId('none'); + setIsSearchMode(true); + setTaskSearchQuery(''); + setIsTaskDropdownOpen(false); // Provide helpful feedback if (previousMode !== mode) { @@ -473,18 +516,30 @@ export function TimerControls({ }; // Drag and drop handlers - const handleDragOver = (e: React.DragEvent) => { + 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(); - setIsDragOver(false); + 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 { @@ -498,6 +553,11 @@ export function TimerControls({ 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.', @@ -530,9 +590,170 @@ 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); + // Small delay to ensure DOM is ready for position calculation + setTimeout(() => { + calculateDropdownPosition(); + }, 0); + }, [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: NodeJS.Timeout; + + 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(); @@ -548,23 +769,66 @@ 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 ( <> - + @@ -579,6 +843,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
@@ -682,27 +954,56 @@ export function TimerControls({
) : (
-
+

- {isDragOver ? "Drop task here to start tracking" : "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"}

- {!isDragOver && ( -

- Drag tasks from the sidebar or select manually below -

- )}
{/* Session Mode Toggle */} @@ -745,65 +1046,383 @@ 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" + : "" + )} + autoFocus={isSearchMode} + /> + +
+ )} + + {/* Dropdown Content */} + {isTaskDropdownOpen && ( +
{ + e.stopPropagation(); + }} + > + {/* Filter Buttons */} +
+
Quick Filters
+
+ + {uniqueBoards.map((board) => ( + + ))} +
+ +
+ + {uniqueLists.map((list) => ( + + ))}
- {task.description && ( -

- {task.description} -

+ + {(taskSearchQuery || (taskFilters.board !== 'all') || (taskFilters.list !== 'all')) && ( +
+ + {getFilteredTasks().length} of {tasks.length} tasks + + +
)} - {task.board_name && task.list_name && ( -
-
- - - {task.board_name} - -
-
- - - {task.list_name} - -
+
+ + {/* 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 d5a418cdd..63549b5d1 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,7 @@ 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,7 +16,15 @@ 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 { Input } from '@tuturuuu/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@tuturuuu/ui/select'; import { AlertCircle, Calendar, @@ -36,6 +44,8 @@ import { Settings, Timer, RefreshCw, + MapPin, + Tag, } from '@tuturuuu/ui/icons'; import { toast } from '@tuturuuu/ui/sonner'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs'; @@ -101,7 +111,7 @@ export default function TimeTrackerContent({ initialData, }: TimeTrackerContentProps) { const { userId: currentUserId, isLoading: isLoadingUser } = useCurrentUser(); - const [activeTab, setActiveTab] = useState('history'); + const [activeTab, setActiveTab] = useState('timer'); const [selectedUserId, setSelectedUserId] = useState(null); // Use React Query for running session to sync with command palette @@ -380,7 +390,7 @@ export default function TimeTrackerContent({ clearInterval(refreshIntervalRef.current); } }; - }, [fetchData, isLoading, retryCount]); + }, [isLoading, retryCount]); // Remove fetchData dependency // Timer effect with better cleanup useEffect(() => { @@ -411,7 +421,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(() => { @@ -430,7 +440,7 @@ export default function TimeTrackerContent({ window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; - }, [fetchData, retryCount]); + }, [retryCount]); // Remove fetchData dependency // Cleanup on unmount useEffect(() => { @@ -459,7 +469,7 @@ 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); @@ -479,6 +489,16 @@ export default function TimeTrackerContent({ // 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 ( @@ -503,25 +523,25 @@ export default function TimeTrackerContent({ {/* 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'} -

+

+ Time Tracker +

+

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

- {!isViewingOtherUser && ( + {!isViewingOtherUser && (
@@ -532,11 +552,11 @@ export default function TimeTrackerContent({
Times updated in real-time
- {(() => { - const today = new Date(); - const dayOfWeek = today.getDay(); + {(() => { + const today = new Date(); + const dayOfWeek = today.getDay(); - if (dayOfWeek === 1) { + if (dayOfWeek === 1) { return ( <> @@ -546,59 +566,59 @@ export default function TimeTrackerContent({
); - } else if (dayOfWeek === 0) { + } else if (dayOfWeek === 0) { return ( <> Week resets tomorrow ); - } else { + } else { return ( <> Week resets Monday ); - } - })()} + } + })()}
- )} + )} - {lastRefresh && ( + {lastRefresh && (
Last updated: {lastRefresh.toLocaleTimeString()} - {isOffline && ( + {isOffline && (
- + Offline
- )} + )}
- )} -
+ )} +
- - -
+ +
+
{/* Quick Actions Carousel */} {!isViewingOtherUser && ( @@ -966,96 +986,263 @@ export default function TimeTrackerContent({ )} - {/* Error Alert with better UX */} - {error && ( - - - -
- - {isOffline - ? 'You are offline. Some features may not work.' - : error} - - {retryCount > 0 && ( -

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

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

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

+ )} +
+ +
+
+ )} {/* New Layout: Analytics sidebar on left, Timer controls and tabs on right */} -
- {/* Left Side: Switchable Sidebar Views */} -
- {/* Sidebar View Switcher */} -
-
- - )} - > - - Tasks - - + {!isViewingOtherUser && ( + )} + +
+
+ + {/* Main Tabs - Timer, History, Categories, Goals */} + + + {/* Tab Content */} + {!isViewingOtherUser && ( + - - Reports - - -
+ 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' && ( @@ -1167,7 +1354,7 @@ export default function TimeTrackerContent({
{/* Activity Heatmap - Enhanced with better header */} - {timerStats.dailyActivity && ( + {timerStats.dailyActivity && (
@@ -1191,10 +1378,10 @@ export default function TimeTrackerContent({
{/* Remove the original header from ActivityHeatmap component */}
- +
)} @@ -1205,9 +1392,10 @@ export default function TimeTrackerContent({ {sidebarView === 'tasks' && (
{/* Tasks Header */} -
-
-
+
+ {/* Header Section */} +
+
@@ -1215,78 +1403,191 @@ export default function TimeTrackerContent({

Task Workspace

-

+

Drag tasks to timer to start tracking 🎯

- {/* Task List */} -
- {tasks.length === 0 ? ( -
- -

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

+ {/* Search and Filter Bar */} +
+
+
+ setTasksSidebarSearch(e.target.value)} + className="h-8 text-xs" + />
- ) : ( - tasks.slice(0, 10).map((task) => ( -
{ - e.dataTransfer.setData('application/json', JSON.stringify({ - type: 'task', - task: task - })); - }} - > -
-
- -
-
-

- {task.name} -

- {task.description && ( -

- {task.description} -

- )} - {task.board_name && task.list_name && ( -
-
- - {task.board_name} - -
-
- - {task.list_name} - + + +
+
+ + {/* 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 + + + +
- )} -
-
- Drag to timer + ))}
+ + {/* Scroll indicator */} + {filteredSidebarTasks.length > 5 && ( +
+
+ Scroll for more + + + +
+
+ )}
-
- )) - )} - - {tasks.length > 10 && ( -
-

- Showing 10 of {tasks.length} tasks -

-
- )} + + ); + })()}
@@ -1351,133 +1652,7 @@ export default function TimeTrackerContent({
)} -
- - {/* Right Side: Timer Controls and Tabs */} -
- {/* Timer Controls - Always visible for current user */} - {!isViewingOtherUser && ( -
- fetchData(false)} - formatTime={formatTime} - formatDuration={formatDuration} - apiCall={apiCall} - /> -
- )} - - {/* Tabs for additional functionality */} - - - - - History - - {!isViewingOtherUser && ( - - - Categories - - )} - - - Goals - - - - - {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} - /> -
-
+
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/packages/ui/src/components/ui/custom/structure.tsx b/packages/ui/src/components/ui/custom/structure.tsx index 17d9f9532..6b0f0f1b8 100644 --- a/packages/ui/src/components/ui/custom/structure.tsx +++ b/packages/ui/src/components/ui/custom/structure.tsx @@ -144,7 +144,7 @@ export function Structure({
From 44bebc5ce5268dfe4240157ebe53902adae7edcf Mon Sep 17 00:00:00 2001 From: Adinorio Date: Mon, 16 Jun 2025 02:20:10 +0800 Subject: [PATCH 3/5] fix(time-tracker): resolved comments & run failure --- .../[wsId]/time-tracker/components/timer-controls.tsx | 8 ++++---- .../[wsId]/time-tracker/time-tracker-content.tsx | 2 +- packages/ui/src/components/ui/custom/structure.tsx | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) 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 a3f3d5ff9..fd769c515 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 @@ -662,10 +662,10 @@ export function TimerControls({ // Open dropdown with position calculation const openDropdown = useCallback(() => { setIsTaskDropdownOpen(true); - // Small delay to ensure DOM is ready for position calculation - setTimeout(() => { + // Use requestAnimationFrame to ensure DOM is ready for position calculation + requestAnimationFrame(() => { calculateDropdownPosition(); - }, 0); + }); }, [calculateDropdownPosition]); // Close dropdown when clicking outside @@ -688,7 +688,7 @@ export function TimerControls({ // Handle scroll and resize events useEffect(() => { - let scrollTimeout: NodeJS.Timeout; + let scrollTimeout: ReturnType; const handleScroll = () => { if (isTaskDropdownOpen) { 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 63549b5d1..bd74da5b6 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 @@ -48,7 +48,7 @@ import { Tag, } from '@tuturuuu/ui/icons'; 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'; diff --git a/packages/ui/src/components/ui/custom/structure.tsx b/packages/ui/src/components/ui/custom/structure.tsx index 6b0f0f1b8..ae7c05d48 100644 --- a/packages/ui/src/components/ui/custom/structure.tsx +++ b/packages/ui/src/components/ui/custom/structure.tsx @@ -141,6 +141,8 @@ export function Structure({ )} + {/* Main content area - overflow-y-auto removed to prevent double scrollbars */} + {/* Body element now handles page-level scrolling */}
Date: Sun, 15 Jun 2025 18:22:39 +0000 Subject: [PATCH 4/5] style: apply prettier formatting --- .../components/timer-controls.tsx | 622 ++++--- .../time-tracker/time-tracker-content.tsx | 1436 +++++++++-------- bun.lock | 4 +- 3 files changed, 1171 insertions(+), 891 deletions(-) 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 fd769c515..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 @@ -146,10 +146,12 @@ export function TimerControls({ }); const [isTaskDropdownOpen, setIsTaskDropdownOpen] = useState(false); const [isSearchMode, setIsSearchMode] = useState(false); - + // Dropdown positioning state - const [dropdownPosition, setDropdownPosition] = useState<'below' | 'above'>('below'); - + const [dropdownPosition, setDropdownPosition] = useState<'below' | 'above'>( + 'below' + ); + // Refs for positioning const dropdownContainerRef = useRef(null); const dropdownContentRef = useRef(null); @@ -203,13 +205,14 @@ export function TimerControls({ 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.', + description: + 'Click Start Timer to begin tracking time for this task.', duration: 3000, }); - + // Close dropdown and exit search mode setIsTaskDropdownOpen(false); setIsSearchMode(false); @@ -530,7 +533,7 @@ export function TimerControls({ const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); dragCounterRef.current--; - + // Only set isDragOver to false when counter reaches 0 if (dragCounterRef.current === 0) { setIsDragOver(false); @@ -546,21 +549,22 @@ export function TimerControls({ 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.', + description: + 'Click Start Timer to begin tracking time for this task.', duration: 3000, }); } @@ -600,7 +604,7 @@ export function TimerControls({ taskFilters.priority === 'all' || String(task.priority) === taskFilters.priority; const matchesStatus = - taskFilters.status === 'all' || + taskFilters.status === 'all' || (task.completed ? 'completed' : 'active') === taskFilters.status; const matchesBoard = taskFilters.board === 'all' || task.board_name === taskFilters.board; @@ -618,8 +622,20 @@ export function TimerControls({ }; // 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)))]; + 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(() => { @@ -630,13 +646,16 @@ export function TimerControls({ 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) { + if ( + spaceBelow >= Math.min(dropdownHeight, 200) || + spaceBelow >= spaceAbove + ) { setDropdownPosition('below'); } else { setDropdownPosition('above'); @@ -646,16 +665,16 @@ export function TimerControls({ // 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; }, []); @@ -673,7 +692,10 @@ export function TimerControls({ 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\\]')) { + if ( + !target.closest('[data-task-dropdown]') && + !target.closest('.absolute.z-\\[100\\]') + ) { setIsTaskDropdownOpen(false); setIsSearchMode(false); } @@ -682,19 +704,20 @@ export function TimerControls({ 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); + 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()) { @@ -716,11 +739,11 @@ export function TimerControls({ 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); @@ -733,9 +756,10 @@ export function TimerControls({ 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'; + 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') { @@ -746,7 +770,9 @@ export function TimerControls({ 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)'); + toast.info( + 'Press ESC while dragging to cancel (drag outside to cancel)' + ); return; } } @@ -780,24 +806,33 @@ export function TimerControls({ 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`); + 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')) { + 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); + const currentIndex = filteredTasks.findIndex( + (task) => task.id === selectedTaskId + ); let nextIndex; - + if (event.key === 'ArrowDown') { - nextIndex = currentIndex < filteredTasks.length - 1 ? currentIndex + 1 : 0; + nextIndex = + currentIndex < filteredTasks.length - 1 ? currentIndex + 1 : 0; } else { - nextIndex = currentIndex > 0 ? currentIndex - 1 : filteredTasks.length - 1; + nextIndex = + currentIndex > 0 ? currentIndex - 1 : filteredTasks.length - 1; } - + const nextTask = filteredTasks[nextIndex]; if (nextTask?.id) { setSelectedTaskId(nextTask.id); @@ -805,7 +840,11 @@ export function TimerControls({ } // Enter to select highlighted task when dropdown is open - if (isTaskDropdownOpen && event.key === 'Enter' && selectedTaskId !== 'none') { + if ( + isTaskDropdownOpen && + event.key === 'Enter' && + selectedTaskId !== 'none' + ) { event.preventDefault(); handleTaskSelectionChange(selectedTaskId); } @@ -821,14 +860,27 @@ export function TimerControls({ document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [isRunning, newSessionTitle, startTimer, stopTimer, pauseTimer, isTaskDropdownOpen, isDraggingTask, selectedTaskId, sessionMode]); + }, [ + isRunning, + newSessionTitle, + startTimer, + stopTimer, + pauseTimer, + isTaskDropdownOpen, + isDraggingTask, + selectedTaskId, + sessionMode, + ]); return ( <> - + @@ -954,55 +1006,61 @@ export function TimerControls({
) : (
-
- -

- {isDragOver - ? "Drop task here to start tracking" - : isDraggingTask - ? "Drag task here to start tracking" - : "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"} +

+ {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'}

@@ -1046,15 +1104,16 @@ export function TimerControls({ - + {tasks.length === 0 ? (
-

+

No tasks available

-

- Create tasks in your project boards to start tracking time +

+ Create tasks in your project boards to start tracking + time

- + + + + + +
-
- ) : null; - })()} - + ) : null; + })()} + {/* Search Mode: Show Input Field */} - {(isSearchMode || !selectedTaskId || selectedTaskId === 'none') && ( + {(isSearchMode || + !selectedTaskId || + selectedTaskId === 'none') && (
setTaskSearchQuery(e.target.value)} + onChange={(e) => + 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" - : "" + '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' + : '' )} autoFocus={isSearchMode} /> @@ -1205,18 +1300,26 @@ export function TimerControls({ openDropdown(); } }} - className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-muted rounded" + className="absolute top-1/2 right-2 -translate-y-1/2 rounded p-1 hover:bg-muted" > - +
@@ -1224,32 +1327,39 @@ export function TimerControls({ {/* Dropdown Content */} {isTaskDropdownOpen && ( -
{ e.stopPropagation(); }} > {/* Filter Buttons */} -
-
Quick Filters
+
+
+ Quick Filters +
))}
- +
))}
- - {(taskSearchQuery || (taskFilters.board !== 'all') || (taskFilters.list !== 'all')) && ( + + {(taskSearchQuery || + taskFilters.board !== 'all' || + taskFilters.list !== 'all') && (
- {getFilteredTasks().length} of {tasks.length} tasks + {getFilteredTasks().length} of{' '} + {tasks.length} tasks
)}
- + {/* Task List */}
{getFilteredTasks().length === 0 ? (
- {taskSearchQuery || (taskFilters.board !== 'all') || (taskFilters.list !== 'all') ? ( + {taskSearchQuery || + taskFilters.board !== 'all' || + taskFilters.list !== 'all' ? ( <> -
No tasks found matching your criteria
+
+ No tasks found matching your criteria +
+ type="button" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setTaskSearchQuery(''); + setTaskFilters({ + board: 'all', + list: 'all', + priority: 'all', + status: 'all', + }); + }} + className="text-xs text-primary hover:underline" + > + Clear filters to see all tasks + ) : ( - "No tasks available" + 'No tasks available' )}
) : ( @@ -1370,7 +1506,7 @@ export function TimerControls({ handleTaskSelectionChange(task.id); } }} - className="w-full p-0 text-left hover:bg-muted/50 focus:bg-muted/50 focus:outline-none transition-colors" + className="w-full p-0 text-left transition-colors hover:bg-muted/50 focus:bg-muted/50 focus:outline-none" >
@@ -1388,22 +1524,23 @@ export function TimerControls({ {task.description}

)} - {task.board_name && task.list_name && ( -
-
- - - {task.board_name} - + {task.board_name && + task.list_name && ( +
+
+ + + {task.board_name} + +
+
+ + + {task.list_name} + +
-
- - - {task.list_name} - -
-
- )} + )}
@@ -1414,7 +1551,6 @@ export function TimerControls({ )}
- {(selectedTaskId === 'none' || !selectedTaskId) && (

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 bd74da5b6..d206b05e4 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 { TimerControls } from './components/timer-controls'; import { UserSelector } from './components/user-selector'; import { useCurrentUser } from './hooks/use-current-user'; @@ -16,37 +15,36 @@ import type { } from '@tuturuuu/types/db'; import { Alert, AlertDescription } from '@tuturuuu/ui/alert'; import { Button } from '@tuturuuu/ui/button'; - -import { Input } from '@tuturuuu/ui/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@tuturuuu/ui/select'; import { AlertCircle, Calendar, - Clock, + CheckCircle, ChevronLeft, ChevronRight, + Clock, Copy, - CheckCircle, + History, + MapPin, + Pause, + RefreshCw, RotateCcw, + Settings, Sparkles, + Tag, Target, + Timer, TrendingUp, WifiOff, Zap, - Pause, - History, - Settings, - Timer, - RefreshCw, - MapPin, - Tag, } 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 } from '@tuturuuu/ui/tabs'; import { cn } from '@tuturuuu/utils/format'; @@ -479,7 +477,8 @@ export default function TimeTrackerContent({ useEffect(() => { const interval = setInterval(() => { const timeSinceLastInteraction = Date.now() - lastUserInteraction; - if (timeSinceLastInteraction >= 15000) { // 15 seconds + if (timeSinceLastInteraction >= 15000) { + // 15 seconds setCarouselView((prev) => (prev === 2 ? 0 : prev + 1)); } }, 15000); @@ -488,8 +487,10 @@ export default function TimeTrackerContent({ }, [lastUserInteraction]); // Sidebar View Switching - const [sidebarView, setSidebarView] = useState<'analytics' | 'tasks' | 'reports' | 'settings'>('analytics'); - + const [sidebarView, setSidebarView] = useState< + 'analytics' | 'tasks' | 'reports' | 'settings' + >('analytics'); + // Drag and drop state for highlighting drop zones const [isDraggingTask, setIsDraggingTask] = useState(false); @@ -523,25 +524,25 @@ export default function TimeTrackerContent({ {/* 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'} -

+

+ Time Tracker +

+

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

- - {!isViewingOtherUser && ( + + {!isViewingOtherUser && (
@@ -552,11 +553,11 @@ export default function TimeTrackerContent({
Times updated in real-time
- {(() => { - const today = new Date(); - const dayOfWeek = today.getDay(); + {(() => { + const today = new Date(); + const dayOfWeek = today.getDay(); - if (dayOfWeek === 1) { + if (dayOfWeek === 1) { return ( <> @@ -566,59 +567,61 @@ export default function TimeTrackerContent({
); - } else if (dayOfWeek === 0) { + } else if (dayOfWeek === 0) { return ( <> Week resets tomorrow ); - } else { + } else { return ( <> Week resets Monday ); - } - })()} + } + })()}
- )} - - {lastRefresh && ( + )} + + {lastRefresh && (
Last updated: {lastRefresh.toLocaleTimeString()} - {isOffline && ( + {isOffline && (
- + Offline
- )} + )}
- )} -
- + )} +
+
- - + + +
-
{/* Quick Actions Carousel */} {!isViewingOtherUser && ( @@ -637,7 +640,7 @@ export default function TimeTrackerContent({ > - +
{[0, 1, 2].map((index) => (
- +
- +
- {carouselView === 0 && "Smart Quick Actions"} - {carouselView === 1 && "Context-Aware Dashboard"} - {carouselView === 2 && "Productivity Command Center"} + {carouselView === 0 && 'Smart Quick Actions'} + {carouselView === 1 && 'Context-Aware Dashboard'} + {carouselView === 2 && 'Productivity Command Center'}
{/* Carousel Content */}
-
@@ -689,18 +692,21 @@ export default function TimeTrackerContent({ - - - )} +
+ )} + + {/* Error Alert with better UX */} + {error && ( + + + +
+ + {isOffline + ? 'You are offline. Some features may not work.' + : error} + + {retryCount > 0 && ( +

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

+ )} +
+ +
+
+ )} {/* New Layout: Analytics sidebar on left, Timer controls and tabs on right */}
@@ -1031,10 +1064,10 @@ export default function TimeTrackerContent({
{/* 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!' + ); + }} + /> +
+
+ )} - {/* Tab Content */} - {!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)} - formatTime={formatTime} - formatDuration={formatDuration} + onCategoriesUpdate={() => fetchData(false)} + readOnly={isViewingOtherUser} 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 && ( + - +

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

+
+ )} + fetchData(false)} + timerStats={timerStats} + onGoalsUpdate={() => fetchData(false)} readOnly={isViewingOtherUser} + formatDuration={formatDuration} 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} - /> -
@@ -1196,10 +1227,10 @@ export default function TimeTrackerContent({