import { useState, useRef } from "react"; import { Play, Square, ChevronDown, Pencil, Check, X, Trash2 } from "lucide-react"; import { useTimer } from "@/contexts/TimerContext"; import { useProjects } from "@/hooks/useProjects"; import { ProjectColorDot } from "@/components/ProjectColorDot"; function TimerDisplay({ totalSeconds }: { totalSeconds: number }) { const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; const pad = (n: number) => n.toString().padStart(2, "0"); return ( {hours > 0 && ( <> {pad(hours)} h > )} {pad(minutes)} m {pad(seconds)} s ); } /** Converts a HH:mm string to an ISO datetime, inferring the correct date. * If the resulting time would be in the future, it is assumed to belong to the previous day. */ function timeInputToIso(timeValue: string): string { const [hours, minutes] = timeValue.split(":").map(Number); const now = new Date(); const candidate = new Date(now); candidate.setHours(hours, minutes, 0, 0); // If the candidate is in the future, roll back one day if (candidate > now) { candidate.setDate(candidate.getDate() - 1); } return candidate.toISOString(); } export function TimerWidget() { const { ongoingTimer, isLoading, elapsedSeconds, startTimer, stopTimer, cancelTimer, updateTimerProject, updateTimerStartTime, } = useTimer(); const { projects } = useProjects(); const [showProjectSelect, setShowProjectSelect] = useState(false); const [error, setError] = useState(null); // Start time editing state const [editingStartTime, setEditingStartTime] = useState(false); const [startTimeInput, setStartTimeInput] = useState(""); const startTimeInputRef = useRef(null); const handleStart = async () => { setError(null); try { await startTimer(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to start timer"); } }; const handleStop = async () => { setError(null); try { await stopTimer(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to stop timer"); } }; const handleCancelTimer = async () => { setError(null); try { await cancelTimer(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to cancel timer"); } }; const handleProjectChange = async (projectId: string) => { setError(null); try { await updateTimerProject(projectId); setShowProjectSelect(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to update project"); } }; const handleClearProject = async () => { setError(null); try { await updateTimerProject(null); setShowProjectSelect(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to clear project"); } }; const handleStartEditStartTime = () => { if (!ongoingTimer) return; const start = new Date(ongoingTimer.startTime); const hh = start.getHours().toString().padStart(2, "0"); const mm = start.getMinutes().toString().padStart(2, "0"); setStartTimeInput(`${hh}:${mm}`); setEditingStartTime(true); // Focus the input on next render setTimeout(() => startTimeInputRef.current?.focus(), 0); }; const handleCancelEditStartTime = () => { setEditingStartTime(false); setError(null); }; const handleConfirmStartTime = async () => { if (!startTimeInput) return; setError(null); try { const iso = timeInputToIso(startTimeInput); await updateTimerStartTime(iso); setEditingStartTime(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to update start time"); } }; const handleStartTimeKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { void handleConfirmStartTime(); } else if (e.key === "Escape") { handleCancelEditStartTime(); } }; if (isLoading) { return ( ); } return ( {ongoingTimer ? ( <> {/* Row 1 (mobile): timer + stop side by side. On sm+ dissolves into the parent flex row via contents. */} {/* Timer Display + Start Time Editor */} {editingStartTime ? ( Started at setStartTimeInput(e.target.value)} onKeyDown={handleStartTimeKeyDown} className="font-mono text-lg font-bold text-gray-900 border border-primary-400 rounded px-2 py-0.5 focus:outline-none focus:ring-2 focus:ring-primary-500 w-28" /> void handleConfirmStartTime()} title="Confirm" className="p-1 text-green-600 hover:text-green-700 hover:bg-green-50 rounded" > ) : ( )} {/* Stop + Cancel Buttons */} void handleCancelTimer()} title="Discard timer" className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors" > Stop {/* Project Selector — full width on mobile, auto on desktop */} setShowProjectSelect(!showProjectSelect)} className="flex items-center space-x-2 px-3 py-2 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors w-full sm:w-auto" > {ongoingTimer.project ? ( <> {ongoingTimer.project.name} > ) : ( Select project... )} {showProjectSelect && ( No project {projects?.map((project) => ( handleProjectChange(project.id)} className="w-full px-4 py-2 text-left text-sm hover:bg-gray-50 flex items-center space-x-2" > {project.name} {project.client.name} ))} )} > ) : ( {/* Stopped Timer Display */} Ready to track time {/* Start Button */} Start )} {error && ( {error} )} ); }
{error}