mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
Merge Tag v0.2.12
This commit is contained in:
@@ -3,55 +3,41 @@
|
||||
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 { 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 {
|
||||
labels: string[]
|
||||
getLabels: () => string[]
|
||||
duration: number
|
||||
type: 'focus' | 'break'
|
||||
}
|
||||
|
||||
const PomoConfigs: Record<PomoConfig['type'], PomoConfig> = {
|
||||
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 t = useTranslations('PomodoroTimer')
|
||||
|
||||
const PomoConfigs: Record<PomoConfig['type'], PomoConfig> = {
|
||||
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)
|
||||
@@ -61,21 +47,23 @@ export default function PomodoroTimer() {
|
||||
const [state, setState] = useState<'started' | 'stopped' | 'paused'>(autoStart ? 'started' : 'stopped')
|
||||
const wakeLock = useRef<WakeLockSentinel | null>(null)
|
||||
const [todayCompletions] = useAtom(pomodoroTodayCompletionsAtom)
|
||||
const currentTimer = useRef<PomoConfig>(PomoConfigs.focus)
|
||||
const [currentLabel, setCurrentLabel] = useState(
|
||||
currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)]
|
||||
)
|
||||
const currentTimerRef = useRef<PomoConfig>(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('Browser does not support wakelock')
|
||||
console.debug(t('wakeLockNotSupported'))
|
||||
return
|
||||
}
|
||||
if (wakeLock.current && !wakeLock.current.released) {
|
||||
console.debug('Wake lock already in use')
|
||||
console.debug(t('wakeLockInUse'))
|
||||
return
|
||||
}
|
||||
if (state === 'started') {
|
||||
@@ -84,7 +72,7 @@ export default function PomodoroTimer() {
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error requesting wake lock:', err)
|
||||
console.error(t('wakeLockRequestError'), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +83,7 @@ export default function PomodoroTimer() {
|
||||
wakeLock.current = null
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error releasing wake lock:', err)
|
||||
console.error(t('wakeLockReleaseError'), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,40 +112,43 @@ export default function PomodoroTimer() {
|
||||
|
||||
// Timer logic
|
||||
useEffect(() => {
|
||||
let interval: ReturnType<typeof setInterval> | null = null
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
if (state === 'started') {
|
||||
if (state === "started") {
|
||||
// Calculate the target end time based on current timeLeft
|
||||
const targetEndTime = Date.now() + timeLeft * 1000
|
||||
const targetEndTime = Date.now() + timeLeft * 1000;
|
||||
|
||||
interval = setInterval(() => {
|
||||
const remaining = Math.floor((targetEndTime - Date.now()) / 1000)
|
||||
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
|
||||
}
|
||||
handleTimerEnd();
|
||||
} else {
|
||||
setTimeLeft(remaining)
|
||||
setTimeLeft(remaining);
|
||||
}
|
||||
}, 1000)
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// return handles any other states
|
||||
return () => {
|
||||
if (interval) clearInterval(interval)
|
||||
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
|
||||
}
|
||||
}, [state, timeLeft, completeHabit, selectedHabit])
|
||||
}
|
||||
|
||||
const toggleTimer = () => {
|
||||
setState(prev => prev === 'started' ? 'paused' : 'started')
|
||||
@@ -165,17 +156,16 @@ export default function PomodoroTimer() {
|
||||
|
||||
const resetTimer = () => {
|
||||
setState("stopped")
|
||||
setTimeLeft(currentTimer.current.duration)
|
||||
setTimeLeft(currentTimerRef.current.duration)
|
||||
}
|
||||
|
||||
const skipTimer = () => {
|
||||
currentTimer.current = currentTimer.current.type === 'focus'
|
||||
currentTimerRef.current = currentTimerRef.current.type === 'focus'
|
||||
? PomoConfigs.break
|
||||
: PomoConfigs.focus
|
||||
resetTimer()
|
||||
setCurrentLabel(
|
||||
currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)]
|
||||
)
|
||||
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) => {
|
||||
@@ -184,7 +174,7 @@ export default function PomodoroTimer() {
|
||||
return `${minutes}:${secs < 10 ? '0' : ''}${secs}`
|
||||
}
|
||||
|
||||
const progress = (timeLeft / currentTimer.current.duration) * 100
|
||||
const progress = (timeLeft / currentTimerRef.current.duration) * 100
|
||||
|
||||
if (!show) return null
|
||||
|
||||
@@ -237,11 +227,11 @@ export default function PomodoroTimer() {
|
||||
<div className={cn(
|
||||
'w-2 h-2 rounded-full flex-none',
|
||||
// order matters here
|
||||
currentTimer.current.type === 'focus' && 'bg-green-500',
|
||||
currentTimerRef.current.type === 'focus' && 'bg-green-500',
|
||||
state === 'started' && 'animate-pulse',
|
||||
state === 'paused' && 'bg-yellow-500',
|
||||
state === 'stopped' && 'bg-red-500',
|
||||
currentTimer.current.type === 'break' && 'bg-blue-500',
|
||||
currentTimerRef.current.type === 'break' && 'bg-blue-500',
|
||||
)} />
|
||||
<div className="font-bold text-foreground">
|
||||
{selectedHabit.name}
|
||||
@@ -249,7 +239,9 @@ export default function PomodoroTimer() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span>{currentTimer.current.type.charAt(0).toUpperCase() + currentTimer.current.type.slice(1)}: {currentLabel}</span>
|
||||
<span>
|
||||
{currentTimerRef.current.type === 'focus' ? t('focusType') : t('breakType')}: {currentLabel}
|
||||
</span>
|
||||
{selectedHabit && selectedHabit.targetCompletions && selectedHabit.targetCompletions > 1 && (
|
||||
<div className="flex justify-center gap-1 mt-2">
|
||||
{(() => {
|
||||
@@ -288,12 +280,12 @@ export default function PomodoroTimer() {
|
||||
{state === "started" ? (
|
||||
<>
|
||||
<Pause className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Pause</span>
|
||||
<span className="hidden sm:inline">{t('pauseButton')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Start</span>
|
||||
<span className="hidden sm:inline">{t('startButton')}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -304,7 +296,7 @@ export default function PomodoroTimer() {
|
||||
className="sm:px-4"
|
||||
>
|
||||
<RotateCw className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Reset</span>
|
||||
<span className="hidden sm:inline">{t('resetButton')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -313,7 +305,7 @@ export default function PomodoroTimer() {
|
||||
className="sm:px-4"
|
||||
>
|
||||
<SkipForward className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Skip</span>
|
||||
<span className="hidden sm:inline">{t('skipButton')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user