'use client' import { Button } from '@/components/ui/button' import { Progress } from '@/components/ui/progress' import { useHabits } from '@/hooks/useHabits' import { habitsAtom, pomodoroAtom, pomodoroTodayCompletionsAtom, settingsAtom } from '@/lib/atoms' import { cn } from '@/lib/utils' import { useAtom } from 'jotai' import { Clock, Minus, Pause, Play, RotateCw, SkipForward, X } from 'lucide-react' import { useEffect, useRef, useState } from 'react' interface PomoConfig { labels: string[] duration: number type: 'focus' | 'break' } const PomoConfigs: Record = { focus: { labels: [ 'Stay Focused', 'You Got This', 'Keep Going', 'Crush It', 'Make It Happen', 'Stay Strong', 'Push Through', 'One Step at a Time', 'You Can Do It', 'Focus and Conquer' ], duration: 25 * 60, type: 'focus', }, break: { labels: [ 'Take a Break', 'Relax and Recharge', 'Breathe Deeply', 'Stretch It Out', 'Refresh Yourself', 'You Deserve This', 'Recharge Your Energy', 'Step Away for a Bit', 'Clear Your Mind', 'Rest and Rejuvenate' ], duration: 5 * 60, type: 'break', }, } export default function PomodoroTimer() { const [settings] = useAtom(settingsAtom) const [pomo, setPomo] = useAtom(pomodoroAtom) const { show, selectedHabitId, autoStart, minimized } = pomo const [habitsData] = useAtom(habitsAtom) const { completeHabit } = useHabits() const selectedHabit = selectedHabitId ? habitsData.habits.find(habit => habit.id === selectedHabitId) : null const [timeLeft, setTimeLeft] = useState(PomoConfigs.focus.duration) const [state, setState] = useState<'started' | 'stopped' | 'paused'>(autoStart ? 'started' : 'stopped') const wakeLock = useRef(null) const [todayCompletions] = useAtom(pomodoroTodayCompletionsAtom) const currentTimer = useRef(PomoConfigs.focus) const [currentLabel, setCurrentLabel] = useState( currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)] ) // Handle wake lock useEffect(() => { const requestWakeLock = async () => { try { if (!('wakeLock' in navigator)) { console.debug('Browser does not support wakelock') return } if (wakeLock.current && !wakeLock.current.released) { console.debug('Wake lock already in use') return } if (state === 'started') { // acquire wake lock wakeLock.current = await navigator.wakeLock.request('screen') return } } catch (err) { console.error('Error requesting wake lock:', err) } } const releaseWakeLock = async () => { try { if (wakeLock.current) { await wakeLock.current.release() wakeLock.current = null } } catch (err) { console.error('Error releasing wake lock:', err) } } const handleVisibilityChange = async () => { if (document.visibilityState === 'hidden') { await releaseWakeLock(); } else if (document.visibilityState === 'visible') { // Always update indicator when tab becomes visible if (state === 'started') { await requestWakeLock(); } } }; if (state === 'started') { document.addEventListener('visibilitychange', handleVisibilityChange); requestWakeLock() } // return handles all other states return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); releaseWakeLock() } }, [state]) // Timer logic useEffect(() => { let interval: ReturnType | null = null if (state === 'started') { // Calculate the target end time based on current timeLeft const targetEndTime = Date.now() + timeLeft * 1000 interval = setInterval(() => { const remaining = Math.floor((targetEndTime - Date.now()) / 1000) if (remaining <= 0) { setState("stopped") const currentTimerType = currentTimer.current.type currentTimer.current = currentTimerType === 'focus' ? PomoConfigs.break : PomoConfigs.focus setTimeLeft(currentTimer.current.duration) setCurrentLabel( currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)] ) // update habits only after focus sessions if (selectedHabit && currentTimerType === 'focus') { completeHabit(selectedHabit) // The atom will automatically update with the new completions } } else { setTimeLeft(remaining) } }, 1000) } // return handles any other states return () => { if (interval) clearInterval(interval) } }, [state, timeLeft, completeHabit, selectedHabit]) const toggleTimer = () => { setState(prev => prev === 'started' ? 'paused' : 'started') } const resetTimer = () => { setState("stopped") setTimeLeft(currentTimer.current.duration) } const skipTimer = () => { currentTimer.current = currentTimer.current.type === 'focus' ? PomoConfigs.break : PomoConfigs.focus resetTimer() setCurrentLabel( currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)] ) } const formatTime = (seconds: number) => { const minutes = Math.floor(seconds / 60) const secs = seconds % 60 return `${minutes}:${secs < 10 ? '0' : ''}${secs}` } const progress = (timeLeft / currentTimer.current.duration) * 100 if (!show) return null return (
{minimized ? ( // minimized version
setPomo(prev => ({ ...prev, minimized: false }))} >
{formatTime(timeLeft)}
{/* Progress bar as bottom border */}
) : ( // full version
{formatTime(timeLeft)}
{selectedHabit && (
{selectedHabit.name}
)} {currentTimer.current.type.charAt(0).toUpperCase() + currentTimer.current.type.slice(1)}: {currentLabel} {selectedHabit && selectedHabit.targetCompletions && selectedHabit.targetCompletions > 1 && (
{(() => { // Show up to 7 items, but no more than the target completions const maxItems = Math.min(7, selectedHabit.targetCompletions) // Calculate start position to center current completion const start = Math.max(0, Math.min(todayCompletions - Math.floor(maxItems / 2), selectedHabit.targetCompletions - maxItems)) return Array.from({ length: maxItems }).map((_, i) => { const cycle = start + i const isCompleted = cycle < todayCompletions const isCurrent = cycle === todayCompletions return (
{cycle + 1}
) }) })()}
)}
)}
) }