From 24c0d58eeea82c13cef92fce9113d0ad9eed9ca3 Mon Sep 17 00:00:00 2001 From: MAZE Date: Sat, 31 Aug 2024 17:11:56 +0330 Subject: [PATCH] feat: add simple countdown timer --- .stylelintrc.json | 3 +- src/components/menu/items/countdown.tsx | 13 ++ src/components/menu/items/index.ts | 1 + src/components/menu/menu.tsx | 6 +- .../toolbox/countdown/countdown.module.css | 128 +++++++++++++++ .../toolbox/countdown/countdown.tsx | 148 ++++++++++++++++++ src/components/toolbox/countdown/index.ts | 1 + src/components/toolbox/index.ts | 1 + 8 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 src/components/menu/items/countdown.tsx create mode 100644 src/components/toolbox/countdown/countdown.module.css create mode 100644 src/components/toolbox/countdown/countdown.tsx create mode 100644 src/components/toolbox/countdown/index.ts diff --git a/.stylelintrc.json b/.stylelintrc.json index aa8289e..c2d8e41 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -8,7 +8,8 @@ "rules": { "import-notation": "string", - "selector-class-pattern": null + "selector-class-pattern": null, + "no-descending-specificity": null }, "overrides": [ diff --git a/src/components/menu/items/countdown.tsx b/src/components/menu/items/countdown.tsx new file mode 100644 index 0000000..dfcc335 --- /dev/null +++ b/src/components/menu/items/countdown.tsx @@ -0,0 +1,13 @@ +import { MdOutlineTimer } from 'react-icons/md/index'; + +import { Item } from '../item'; + +interface CountdownProps { + open: () => void; +} + +export function Countdown({ open }: CountdownProps) { + return ( + } label="Countdown Timer" onClick={open} /> + ); +} diff --git a/src/components/menu/items/index.ts b/src/components/menu/items/index.ts index 079c410..7c63287 100644 --- a/src/components/menu/items/index.ts +++ b/src/components/menu/items/index.ts @@ -9,3 +9,4 @@ export { BreathingExercise as BreathingExerciseItem } from './breathing-exercise export { Pomodoro as PomodoroItem } from './pomodoro'; export { Notepad as NotepadItem } from './notepad'; export { Todo as TodoItem } from './todo'; +export { Countdown as CountdownItem } from './countdown'; diff --git a/src/components/menu/menu.tsx b/src/components/menu/menu.tsx index 1b4d342..30d4c4a 100644 --- a/src/components/menu/menu.tsx +++ b/src/components/menu/menu.tsx @@ -16,6 +16,7 @@ import { PomodoroItem, NotepadItem, TodoItem, + CountdownItem, } from './items'; import { Divider } from './divider'; import { ShareLinkModal } from '@/components/modals/share-link'; @@ -23,7 +24,7 @@ import { PresetsModal } from '@/components/modals/presets'; import { ShortcutsModal } from '@/components/modals/shortcuts'; import { SleepTimerModal } from '@/components/modals/sleep-timer'; import { BreathingExerciseModal } from '../modals/breathing'; -import { Pomodoro, Notepad, Todo } from '../toolbox'; +import { Pomodoro, Notepad, Todo, Countdown } from '../toolbox'; import { fade, mix, slideY } from '@/lib/motion'; import { useSoundStore } from '@/stores/sound'; @@ -39,6 +40,7 @@ export function Menu() { const initial = useMemo( () => ({ breathing: false, + countdown: false, notepad: false, pomodoro: false, presets: false, @@ -115,6 +117,7 @@ export function Menu() { open('sleepTimer')} /> + open('countdown')} /> open('pomodoro')} /> open('notepad')} /> open('todo')} /> @@ -153,6 +156,7 @@ export function Menu() { /> close('notepad')} /> close('todo')} /> + close('countdown')} /> close('presets')} /> void; + show: boolean; +} + +export function Countdown({ onClose, show }: CountdownProps) { + const [hours, setHours] = useState(0); + const [minutes, setMinutes] = useState(0); + const [seconds, setSeconds] = useState(0); + const [timeLeft, setTimeLeft] = useState(0); + const [initialTime, setInitialTime] = useState(0); + const [isActive, setIsActive] = useState(false); + const [isFormVisible, setIsFormVisible] = useState(true); + + const alarm = useSoundEffect('/sounds/alarm.mp3'); + + useEffect(() => { + let timer: NodeJS.Timeout; + + if (isActive && timeLeft > 0) { + timer = setTimeout(() => setTimeLeft(timeLeft - 1), 1000); + } else if (timeLeft === 0 && isActive) { + alarm.play(); + setIsActive(false); + setIsFormVisible(true); + } + + return () => clearTimeout(timer); + }, [isActive, timeLeft, alarm]); + + const handleStart = useCallback(() => { + if (hours > 0 || minutes > 0 || seconds > 0) { + const totalTime = + (hours || 0) * 3600 + (minutes || 0) * 60 + (seconds || 0); + + setTimeLeft(totalTime); + setInitialTime(totalTime); + setIsActive(true); + setIsFormVisible(false); + } + }, [hours, minutes, seconds]); + + const handleBack = useCallback(() => { + setIsActive(false); + setIsFormVisible(true); + setTimeLeft(0); + }, []); + + const toggleTimer = useCallback(() => { + setIsActive(prev => !prev); + }, []); + + const formatTime = useCallback((time: number) => { + const hrs = Math.floor(time / 3600); + const mins = Math.floor((time % 3600) / 60); + const secs = time % 60; + + return `${padNumber(hrs)}:${padNumber(mins)}:${padNumber(secs)}`; + }, []); + + const elapsedTime = initialTime - timeLeft; + + return ( + +
+

Countdown Timer

+

Super simple countdown timer.

+
+ + {isFormVisible ? ( +
+
+ setHours(Math.max(0, parseInt(e.target.value)))} + /> + + : + + + setMinutes(Math.max(0, Math.min(59, parseInt(e.target.value)))) + } + /> + + : + + + setSeconds(Math.max(0, Math.min(59, parseInt(e.target.value)))) + } + /> +
+ +
+ +
+
+ ) : ( +
+
+

- {formatTime(elapsedTime)}

+ {formatTime(timeLeft)} +
+ +
+ + + +
+
+ )} +
+ ); +} diff --git a/src/components/toolbox/countdown/index.ts b/src/components/toolbox/countdown/index.ts new file mode 100644 index 0000000..a7731c7 --- /dev/null +++ b/src/components/toolbox/countdown/index.ts @@ -0,0 +1 @@ +export { Countdown } from './countdown'; diff --git a/src/components/toolbox/index.ts b/src/components/toolbox/index.ts index 5e568f6..0b100ca 100644 --- a/src/components/toolbox/index.ts +++ b/src/components/toolbox/index.ts @@ -1,3 +1,4 @@ export { Notepad } from './notepad'; export { Pomodoro } from './pomodoro'; export { Todo } from './todo'; +export { Countdown } from './countdown';