tududi/frontend/components/Shared/PomodoroTimer.tsx
Chris 03f38f05dc
Setup intelligence (#84)
* Add next suggestions and remove console logs

* Add pomodoro timer

* Add pomodoro switch in settings

* Fix pomodoro setting

* Add timezones to settings

* Fix an issue with password reset

* Cleanup

* Sort tags alphabetically

* Clean up today's view

* Add an indicator for repeatedly added to today

* Refactor tags

* Add due date today item

* Move recurrence to the subtitle area

* Fix today layout

* Add a badge to Inbox items

* Move inbox badge to sidebar

* Add quotes and progress bar

* Add translations for quotes

* Fix test issues

* Add helper script for docker local

* Set up overdue tasks

* Add  linux/arm/v7 build to deploy script

* Add  linux/arm/v7 build to deploy script pt2

* Fix an issue with helmet and SSL

* Add volume db persistence

* Fix cog icon issues
2025-06-27 14:02:18 +03:00

226 lines
No EOL
6.9 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import { PlayIcon, PauseIcon, ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { useTranslation } from 'react-i18next';
interface PomodoroTimerProps {
className?: string;
}
const POMODORO_STORAGE_KEY = 'tududi_pomodoro_timer';
const DEFAULT_TIME = 25 * 60; // 25 minutes in seconds
interface PomodoroState {
isActive: boolean;
timeLeft: number;
isRunning: boolean;
startTime?: number;
}
const PomodoroTimer: React.FC<PomodoroTimerProps> = ({ className = '' }) => {
const { t } = useTranslation();
const [isActive, setIsActive] = useState(false);
const [timeLeft, setTimeLeft] = useState(DEFAULT_TIME);
const [isRunning, setIsRunning] = useState(false);
const [showCompletionMessage, setShowCompletionMessage] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// Load state from localStorage on mount
useEffect(() => {
const savedState = localStorage.getItem(POMODORO_STORAGE_KEY);
if (savedState) {
try {
const state: PomodoroState = JSON.parse(savedState);
if (state.isActive) {
setIsActive(true);
setTimeLeft(state.timeLeft);
setIsRunning(state.isRunning);
// If timer was running, calculate how much time has passed
if (state.isRunning && state.startTime) {
const elapsed = Math.floor((Date.now() - state.startTime) / 1000);
const newTimeLeft = Math.max(0, state.timeLeft - elapsed);
setTimeLeft(newTimeLeft);
if (newTimeLeft > 0) {
setIsRunning(true);
} else {
setIsRunning(false);
}
}
}
} catch (error) {
console.error('Failed to load pomodoro state:', error);
}
}
}, []);
// Save state to localStorage whenever it changes
useEffect(() => {
const state: PomodoroState = {
isActive,
timeLeft,
isRunning,
startTime: isRunning ? Date.now() - (DEFAULT_TIME - timeLeft) * 1000 : undefined
};
localStorage.setItem(POMODORO_STORAGE_KEY, JSON.stringify(state));
}, [isActive, timeLeft, isRunning]);
useEffect(() => {
if (isRunning && timeLeft > 0) {
intervalRef.current = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
setIsRunning(false);
setShowCompletionMessage(true);
return 0;
}
return prev - 1;
});
}, 1000);
} else {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isRunning, timeLeft]);
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const handleTomatoClick = () => {
setIsActive(true);
setTimeLeft(DEFAULT_TIME);
setIsRunning(false);
};
const handlePlayPause = () => {
setIsRunning(!isRunning);
};
const handleReset = () => {
setIsRunning(false);
setTimeLeft(DEFAULT_TIME);
setShowCompletionMessage(false);
};
const handleClose = () => {
setIsActive(false);
setIsRunning(false);
setTimeLeft(DEFAULT_TIME);
setShowCompletionMessage(false);
localStorage.removeItem(POMODORO_STORAGE_KEY);
};
// Tomato SVG Icon
const TomatoIcon = () => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className="cursor-pointer hover:scale-110 transition-transform"
>
{/* Tomato body */}
<path
d="M12 22c-4.5 0-8-3-8-7 0-2 1-4 2-5.5C7 8 8.5 7 10 7c1 0 2 .5 2 .5s1-.5 2-.5c1.5 0 3 1 4 2.5 1 1.5 2 3.5 2 5.5 0 4-3.5 7-8 7z"
fill="#e74c3c"
stroke="#c0392b"
strokeWidth="1"
/>
{/* Tomato stem */}
<path
d="M10 7c0-1 .5-2 1-3 .5 1 1.5 2 1.5 3"
fill="none"
stroke="#27ae60"
strokeWidth="2"
strokeLinecap="round"
/>
{/* Tomato leaf */}
<path
d="M11 4c-1 0-2 1-2 2"
fill="none"
stroke="#27ae60"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
if (!isActive) {
return (
<div className={`flex items-center ${className}`} onClick={handleTomatoClick}>
<TomatoIcon />
</div>
);
}
return (
<div className={`relative flex items-center space-x-2 ${className}`}>
<div className="flex items-center space-x-2 bg-gray-100 dark:bg-gray-800 rounded-lg px-3 py-1">
<span className="text-sm font-mono font-semibold text-gray-900 dark:text-white">
{formatTime(timeLeft)}
</span>
<button
onClick={handlePlayPause}
className="flex items-center justify-center p-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
aria-label={isRunning ? t('pomodoro.pause') : t('pomodoro.play')}
>
{isRunning ? (
<PauseIcon className="h-3 w-3" />
) : (
<PlayIcon className="h-3 w-3" />
)}
</button>
<button
onClick={handleReset}
className="flex items-center justify-center p-1 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors"
aria-label={t('pomodoro.reset')}
>
<ArrowPathIcon className="h-3 w-3" />
</button>
<button
onClick={handleClose}
className="flex items-center justify-center p-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
aria-label={t('pomodoro.close')}
>
<XMarkIcon className="h-3 w-3" />
</button>
</div>
{/* Completion Message */}
{showCompletionMessage && (
<div className="absolute top-full mt-2 right-0 bg-green-100 dark:bg-green-900 border border-green-300 dark:border-green-700 text-green-800 dark:text-green-200 px-3 py-2 rounded-lg shadow-lg z-50 whitespace-nowrap">
<div className="flex items-center space-x-2 mb-2">
<span className="text-sm font-medium">🍅 {t('pomodoro.complete')}</span>
</div>
<p className="text-xs mb-3">{t('pomodoro.completeMessage')}</p>
<button
onClick={() => {
setShowCompletionMessage(false);
setIsActive(false);
setTimeLeft(DEFAULT_TIME);
localStorage.removeItem(POMODORO_STORAGE_KEY);
}}
className="w-full text-xs px-3 py-1 bg-green-600 dark:bg-green-700 text-white rounded hover:bg-green-700 dark:hover:bg-green-600 transition-colors"
>
{t('pomodoro.done')}
</button>
</div>
)}
</div>
);
};
export default PomodoroTimer;