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 (
+
+
+
+ {!running && (
+
+
+
+ setHours(e.target.value === '' ? '' : e.target.value)
+ }
+ />
+
+ )}
+ {!running && (
+
+
+
+ setMinutes(e.target.value === '' ? '' : e.target.value)
+ }
+ />
+
+ )}
+ {running ?
: null}
+
+ }
+ smallIcon
+ tooltip="Reset"
+ onClick={handleReset}
+ />
+ {!running && (
+ }
+ smallIcon
+ tooltip={'Start'}
+ onClick={handleStart}
+ />
+ )}
+
+
+
+ );
+}
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)}
-
- );
-}