'use client' import { Button } from '@/components/ui/button' import { Progress } from '@/components/ui/progress' import { useHabits } from '@/hooks/useHabits' import { habitsAtom, pomodoroAtom, pomodoroTodayCompletionsAtom } from '@/lib/atoms' import { cn } from '@/lib/utils' import { useAtom } from 'jotai' import { Clock, Minus, Pause, Play, RotateCw, SkipForward, X } from 'lucide-react' import { useTranslations } from 'next-intl' import { useEffect, useRef, useState } from 'react' 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 [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, t]) // Timer logic useEffect(() => { 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 } }; 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, timeLeft, PomoConfigs.break, PomoConfigs.focus, completeHabit, selectedHabit]); 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}
) }) })()}
)}
)}
) }