From 71b62ed3dd365744435dc4499b9c53684f72849c Mon Sep 17 00:00:00 2001 From: Jef Roelandt Date: Sat, 27 Apr 2024 19:06:46 +0200 Subject: [PATCH] feat: add sleep timer --- .../button/button.module.css | 0 .../pomodoro => generic}/button/button.tsx | 0 .../pomodoro => generic}/button/index.ts | 0 .../pomodoro => generic}/timer/index.ts | 0 .../timer/timer.module.css | 0 src/components/generic/timer/timer.tsx | 36 +++++ src/components/menu/items/index.ts | 1 + src/components/menu/items/sleep-timer.tsx | 18 +++ src/components/menu/menu.tsx | 10 ++ src/components/modals/shortcuts/shortcuts.tsx | 4 + src/components/modals/sleep-timer/index.ts | 1 + .../modals/sleep-timer/sleep-timer.module.css | 45 +++++++ .../modals/sleep-timer/sleep-timer.tsx | 127 ++++++++++++++++++ src/components/toolbox/pomodoro/pomodoro.tsx | 4 +- .../toolbox/pomodoro/timer/timer.tsx | 15 --- 15 files changed, 244 insertions(+), 17 deletions(-) rename src/components/{toolbox/pomodoro => generic}/button/button.module.css (100%) rename src/components/{toolbox/pomodoro => generic}/button/button.tsx (100%) rename src/components/{toolbox/pomodoro => generic}/button/index.ts (100%) rename src/components/{toolbox/pomodoro => generic}/timer/index.ts (100%) rename src/components/{toolbox/pomodoro => generic}/timer/timer.module.css (100%) create mode 100644 src/components/generic/timer/timer.tsx create mode 100644 src/components/menu/items/sleep-timer.tsx create mode 100644 src/components/modals/sleep-timer/index.ts create mode 100644 src/components/modals/sleep-timer/sleep-timer.module.css create mode 100644 src/components/modals/sleep-timer/sleep-timer.tsx delete mode 100644 src/components/toolbox/pomodoro/timer/timer.tsx diff --git a/src/components/toolbox/pomodoro/button/button.module.css b/src/components/generic/button/button.module.css similarity index 100% rename from src/components/toolbox/pomodoro/button/button.module.css rename to src/components/generic/button/button.module.css diff --git a/src/components/toolbox/pomodoro/button/button.tsx b/src/components/generic/button/button.tsx similarity index 100% rename from src/components/toolbox/pomodoro/button/button.tsx rename to src/components/generic/button/button.tsx diff --git a/src/components/toolbox/pomodoro/button/index.ts b/src/components/generic/button/index.ts similarity index 100% rename from src/components/toolbox/pomodoro/button/index.ts rename to src/components/generic/button/index.ts diff --git a/src/components/toolbox/pomodoro/timer/index.ts b/src/components/generic/timer/index.ts similarity index 100% rename from src/components/toolbox/pomodoro/timer/index.ts rename to src/components/generic/timer/index.ts diff --git a/src/components/toolbox/pomodoro/timer/timer.module.css b/src/components/generic/timer/timer.module.css similarity index 100% rename from src/components/toolbox/pomodoro/timer/timer.module.css rename to src/components/generic/timer/timer.module.css diff --git a/src/components/generic/timer/timer.tsx b/src/components/generic/timer/timer.tsx new file mode 100644 index 0000000..6d65991 --- /dev/null +++ b/src/components/generic/timer/timer.tsx @@ -0,0 +1,36 @@ +import { padNumber } from '@/helpers/number'; + +import styles from './timer.module.css'; + +interface TimerProps { + displayHours?: boolean; + timer: number; +} + +export function Timer({ displayHours = false, timer }: TimerProps) { + let hours = Math.floor(timer / 3600); + let minutes = Math.floor((timer % 3600) / 60); + let seconds = timer % 60; + + hours = isNaN(hours) ? 0 : hours; + minutes = isNaN(minutes) ? 0 : minutes; + seconds = isNaN(seconds) ? 0 : seconds; + + const formattedHours = padNumber(hours); + const formattedMinutes = padNumber(minutes); + const formattedSeconds = padNumber(seconds); + + return ( +
+ {displayHours ? ( + <> + {formattedHours}:{formattedMinutes}:{formattedSeconds} + + ) : ( + <> + {formattedMinutes}:{formattedSeconds} + + )} +
+ ); +} diff --git a/src/components/menu/items/index.ts b/src/components/menu/items/index.ts index 0343348..05a7348 100644 --- a/src/components/menu/items/index.ts +++ b/src/components/menu/items/index.ts @@ -6,3 +6,4 @@ export { Source as SourceItem } from './source'; export { Pomodoro as PomodoroItem } from './pomodoro'; export { Presets as PresetsItem } from './presets'; export { Shortcuts as ShortcutsItem } from './shortcuts'; +export { SleepTimer } from './sleep-timer'; diff --git a/src/components/menu/items/sleep-timer.tsx b/src/components/menu/items/sleep-timer.tsx new file mode 100644 index 0000000..d1e2820 --- /dev/null +++ b/src/components/menu/items/sleep-timer.tsx @@ -0,0 +1,18 @@ +import { IoMoonSharp } from 'react-icons/io5/index'; + +import { Item } from '../item'; + +interface SleepTimerProps { + open: () => void; +} + +export function SleepTimer({ open }: SleepTimerProps) { + return ( + } + label="Sleep timer" + shortcut="Shift + T" + onClick={open} + /> + ); +} diff --git a/src/components/menu/menu.tsx b/src/components/menu/menu.tsx index 81b95a8..d4e5e0b 100644 --- a/src/components/menu/menu.tsx +++ b/src/components/menu/menu.tsx @@ -13,11 +13,13 @@ import { PomodoroItem, PresetsItem, ShortcutsItem, + SleepTimer, } from './items'; import { Divider } from './divider'; import { ShareLinkModal } from '@/components/modals/share-link'; import { PresetsModal } from '@/components/modals/presets'; import { ShortcutsModal } from '@/components/modals/shortcuts'; +import { SleepTimerModal } from '@/components/modals/sleep-timer'; import { Notepad, Pomodoro } from '@/components/toolbox'; import { fade, mix, slideY } from '@/lib/motion'; import { useSoundStore } from '@/store'; @@ -37,6 +39,7 @@ export function Menu() { presets: false, shareLink: false, shortcuts: false, + sleepTimer: false, }), [], ); @@ -64,6 +67,7 @@ export function Menu() { useHotkeys('shift+alt+p', () => open('presets')); useHotkeys('shift+h', () => open('shortcuts')); useHotkeys('shift+s', () => open('shareLink'), { enabled: !noSelected }); + useHotkeys('shift+t', () => open('sleepTimer')); useCloseListener(closeAll); @@ -71,6 +75,7 @@ export function Menu() { return ( <> +
setIsOpen(o)}> @@ -103,6 +108,7 @@ export function Menu() { open('notepad')} /> open('pomodoro')} /> + open('sleepTimer')} /> open('shortcuts')} /> @@ -133,6 +139,10 @@ export function Menu() { show={modals.pomodoro} onClose={() => close('pomodoro')} /> + close('sleepTimer')} + /> ); } diff --git a/src/components/modals/shortcuts/shortcuts.tsx b/src/components/modals/shortcuts/shortcuts.tsx index ee7a743..786a3c1 100644 --- a/src/components/modals/shortcuts/shortcuts.tsx +++ b/src/components/modals/shortcuts/shortcuts.tsx @@ -29,6 +29,10 @@ export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) { keys: ['Shift', 'P'], label: 'Pomodoro Timer', }, + { + keys: ['Shift', 'T'], + label: 'Sleep Timer', + }, { keys: ['Shift', 'Space'], label: 'Toggle Play', diff --git a/src/components/modals/sleep-timer/index.ts b/src/components/modals/sleep-timer/index.ts new file mode 100644 index 0000000..824c4d9 --- /dev/null +++ b/src/components/modals/sleep-timer/index.ts @@ -0,0 +1 @@ +export { SleepTimerModal } from './sleep-timer'; diff --git a/src/components/modals/sleep-timer/sleep-timer.module.css b/src/components/modals/sleep-timer/sleep-timer.module.css new file mode 100644 index 0000000..3291906 --- /dev/null +++ b/src/components/modals/sleep-timer/sleep-timer.module.css @@ -0,0 +1,45 @@ +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + + & .title { + font-size: var(--font-sm); + font-weight: 500; + color: var(--color-foreground-subtle); + } +} + +.controls { + display: flex; + flex-flow: column wrap; + align-items: flex-start; + margin-top: 8px; + + & .inputContainer { + display: flex; + align-items: center; + + & .input { + display: block; + height: 40px; + padding: 0 8px; + color: var(--color-foreground); + background-color: var(--color-neutral-50); + border: 1px solid var(--color-neutral-200); + border-radius: 4px; + outline: none; + } + + & .label { + width: 100px; + } + } + + & .buttons { + display: flex; + justify-content: flex-end; + width: 100%; + } +} diff --git a/src/components/modals/sleep-timer/sleep-timer.tsx b/src/components/modals/sleep-timer/sleep-timer.tsx new file mode 100644 index 0000000..49eb74c --- /dev/null +++ b/src/components/modals/sleep-timer/sleep-timer.tsx @@ -0,0 +1,127 @@ +import { useEffect, useState, useCallback } from 'react'; +import { Modal } from '@/components/modal'; +import { FaPlay, FaUndo } from 'react-icons/fa/index'; +import { useSoundStore } from '@/store'; + +import { Button } from '@/components/generic/button'; +import { Timer } from '@/components/generic/timer'; + +import styles from './sleep-timer.module.css'; + +interface SleepTimerModalProps { + onClose: () => void; + show: boolean; +} + +export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) { + const [hours, setHours] = useState('0'); + const [minutes, setMinutes] = useState('0'); + const [running, setRunning] = useState(false); + const [timeLeft, setTimeLeft] = useState(0); + const [timerId, setTimerId] = useState(undefined); + + const pause = useSoundStore(state => state.pause); + + const calculateTotalSeconds = useCallback((): number => { + return ( + (hours === '' ? 0 : parseInt(hours)) * 3600 + + (minutes === '' ? 0 : parseInt(minutes)) * 60 + ); + }, [minutes, hours]); + + useEffect(() => { + setTimeLeft(calculateTotalSeconds()); + }, [calculateTotalSeconds]); + + // Handle multiple clicks on this. Only the latest click should be taken into account + const handleStart = () => { + if (timerId) clearInterval(timerId); + + setTimeLeft(calculateTotalSeconds); + setRunning(true); + + if (timeLeft > 0) { + const newTimerId = setInterval(() => { + setTimeLeft(prevTimeLeft => { + const newTimeLeft = prevTimeLeft - 1; + if (newTimeLeft <= 0) { + clearInterval(newTimerId); + pause(); + setRunning(false); + return 0; + } + return newTimeLeft; + }); + }, 1000); + + setTimerId(newTimerId); + } + }; + + const handleReset = () => { + if (timerId) clearInterval(timerId); + setTimeLeft(0); + setRunning(false); + }; + + return ( + +
+

Sleep Timer

+
+
+ {!running && ( +
+ + + setHours(e.target.value === '' ? '' : e.target.value) + } + /> +
+ )} + {!running && ( +
+ + + setMinutes(e.target.value === '' ? '' : e.target.value) + } + /> +
+ )} + {running ? : null} +
+
+
+
+ ); +} diff --git a/src/components/toolbox/pomodoro/pomodoro.tsx b/src/components/toolbox/pomodoro/pomodoro.tsx index 072df5c..0fc5825 100644 --- a/src/components/toolbox/pomodoro/pomodoro.tsx +++ b/src/components/toolbox/pomodoro/pomodoro.tsx @@ -3,9 +3,9 @@ import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index'; import { IoMdSettings } from 'react-icons/io/index'; import { Modal } from '@/components/modal'; +import { Button } from '@/components/generic/button'; +import { Timer } from '@/components/generic/timer'; import { Tabs } from './tabs'; -import { Timer } from './timer'; -import { Button } from './button'; import { Setting } from './setting'; import { useLocalStorage } from '@/hooks/use-local-storage'; diff --git a/src/components/toolbox/pomodoro/timer/timer.tsx b/src/components/toolbox/pomodoro/timer/timer.tsx deleted file mode 100644 index 81a25b2..0000000 --- a/src/components/toolbox/pomodoro/timer/timer.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { padNumber } from '@/helpers/number'; - -import styles from './timer.module.css'; - -interface TimerProps { - timer: number; -} - -export function Timer({ timer }: TimerProps) { - return ( -
- {padNumber(Math.floor(timer / 60))}:{padNumber(timer % 60)} -
- ); -}