'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { Button } from '@/components/ui/button' import { Progress } from '@/components/ui/progress' import { Play, Pause, RotateCw, Minus, X, Clock, SkipForward } from 'lucide-react' import { cn } from '@/lib/utils' import { useAtom } from 'jotai' import { useTranslations } from 'next-intl' import { settingsAtom, pomodoroAtom, habitsAtom, pomodoroTodayCompletionsAtom } from '@/lib/atoms' // import { getCompletionsForDate, getTodayInTimezone } from '@/lib/utils' // Not used after pomodoroTodayCompletionsAtom import { useHabits } from '@/hooks/useHabits' interface PomoConfig { getLabels: () => string[] duration: number type: 'focus' | 'break' } export default function PomodoroTimer() { const t = useTranslations('PomodoroTimer') const PomoConfigs: Record = { focus: { getLabels: () => [ t('focusLabel1'), t('focusLabel2'), t('focusLabel3'), t('focusLabel4'), t('focusLabel5'), t('focusLabel6'), t('focusLabel7'), t('focusLabel8'), t('focusLabel9'), t('focusLabel10') ], duration: 25 * 60, type: 'focus', }, break: { getLabels: () => [ t('breakLabel1'), t('breakLabel2'), t('breakLabel3'), t('breakLabel4'), t('breakLabel5'), t('breakLabel6'), t('breakLabel7'), t('breakLabel8'), t('breakLabel9'), t('breakLabel10') ], duration: 5 * 60, type: 'break', }, } 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 currentTimerRef = useRef(PomoConfigs.focus) const [currentLabel, setCurrentLabel] = useState(() => { const labels = currentTimerRef.current.getLabels(); return labels[Math.floor(Math.random() * labels.length)]; }); // Handle wake lock useEffect(() => { const requestWakeLock = async () => { try { if (!('wakeLock' in navigator)) { console.debug(t('wakeLockNotSupported')) return } if (wakeLock.current && !wakeLock.current.released) { console.debug(t('wakeLockInUse')) return } if (state === 'started') { // acquire wake lock wakeLock.current = await navigator.wakeLock.request('screen') return } } catch (err) { console.error(t('wakeLockRequestError'), err) } } const releaseWakeLock = async () => { try { if (wakeLock.current) { await wakeLock.current.release() wakeLock.current = null } } catch (err) { console.error(t('wakeLockReleaseError'), 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) { handleTimerEnd() } else { setTimeLeft(remaining) } }, 1000) } // return handles any other states return () => { if (interval) clearInterval(interval) } }, [state]) const handleTimerEnd = async () => { setState("stopped") const currentTimerType = currentTimerRef.current.type currentTimerRef.current = currentTimerType === 'focus' ? PomoConfigs.break : PomoConfigs.focus setTimeLeft(currentTimerRef.current.duration) const newLabels = currentTimerRef.current.getLabels(); setCurrentLabel(newLabels[Math.floor(Math.random() * newLabels.length)]) // update habits only after focus sessions if (selectedHabit && currentTimerType === 'focus') { await completeHabit(selectedHabit) // The atom will automatically update with the new completions } } const toggleTimer = () => { setState(prev => prev === 'started' ? 'paused' : 'started') } const resetTimer = () => { setState("stopped") setTimeLeft(currentTimerRef.current.duration) } const skipTimer = () => { currentTimerRef.current = currentTimerRef.current.type === 'focus' ? PomoConfigs.break : PomoConfigs.focus resetTimer() // This will also reset timeLeft to the new timer's duration const newLabels = currentTimerRef.current.getLabels(); setCurrentLabel(newLabels[Math.floor(Math.random() * newLabels.length)]) } const formatTime = (seconds: number) => { const minutes = Math.floor(seconds / 60) const secs = seconds % 60 return `${minutes}:${secs < 10 ? '0' : ''}${secs}` } const progress = (timeLeft / currentTimerRef.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}
)} {currentTimerRef.current.type === 'focus' ? t('focusType') : t('breakType')}: {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}
) }) })()}
)}
)}
) }