Skip to content

Commit

Permalink
feat: add sleep timer
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperMeepBoy committed Apr 28, 2024
1 parent 60f167c commit 71b62ed
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 17 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
36 changes: 36 additions & 0 deletions src/components/generic/timer/timer.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.timer}>
{displayHours ? (
<>
{formattedHours}:{formattedMinutes}:{formattedSeconds}
</>
) : (
<>
{formattedMinutes}:{formattedSeconds}
</>
)}
</div>
);
}
1 change: 1 addition & 0 deletions src/components/menu/items/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
18 changes: 18 additions & 0 deletions src/components/menu/items/sleep-timer.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Item
icon={<IoMoonSharp />}
label="Sleep timer"
shortcut="Shift + T"
onClick={open}
/>
);
}
10 changes: 10 additions & 0 deletions src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,6 +39,7 @@ export function Menu() {
presets: false,
shareLink: false,
shortcuts: false,
sleepTimer: false,
}),
[],
);
Expand Down Expand Up @@ -64,13 +67,15 @@ 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);

const variants = mix(fade(), slideY());

return (
<>
<Divider />
<div className={styles.wrapper}>
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
<DropdownMenu.Trigger asChild>
Expand Down Expand Up @@ -103,6 +108,7 @@ export function Menu() {
<Divider />
<NotepadItem open={() => open('notepad')} />
<PomodoroItem open={() => open('pomodoro')} />
<SleepTimer open={() => open('sleepTimer')} />

<Divider />
<ShortcutsItem open={() => open('shortcuts')} />
Expand Down Expand Up @@ -133,6 +139,10 @@ export function Menu() {
show={modals.pomodoro}
onClose={() => close('pomodoro')}
/>
<SleepTimerModal
show={modals.sleepTimer}
onClose={() => close('sleepTimer')}
/>
</>
);
}
4 changes: 4 additions & 0 deletions src/components/modals/shortcuts/shortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/components/modals/sleep-timer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SleepTimerModal } from './sleep-timer';
45 changes: 45 additions & 0 deletions src/components/modals/sleep-timer/sleep-timer.module.css
Original file line number Diff line number Diff line change
@@ -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%;
}
}
127 changes: 127 additions & 0 deletions src/components/modals/sleep-timer/sleep-timer.tsx
Original file line number Diff line number Diff line change
@@ -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<string>('0');
const [minutes, setMinutes] = useState<string>('0');
const [running, setRunning] = useState(false);
const [timeLeft, setTimeLeft] = useState(0);
const [timerId, setTimerId] = useState<NodeJS.Timeout | undefined>(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 (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Sleep Timer</h2>
</header>
<div className={styles.controls}>
{!running && (
<div className={styles.inputContainer}>
<label className={styles.label} htmlFor="hours">
Hours:
</label>
<input
className={styles.input}
id="hours"
min="0"
type="number"
value={hours}
onChange={e =>
setHours(e.target.value === '' ? '' : e.target.value)
}
/>
</div>
)}
{!running && (
<div className={styles.inputContainer}>
<label className={styles.label} htmlFor="minutes">
Minutes:
</label>
<input
className={styles.input}
max="59"
min="0"
type="number"
value={minutes}
onChange={e =>
setMinutes(e.target.value === '' ? '' : e.target.value)
}
/>
</div>
)}
{running ? <Timer displayHours={true} timer={timeLeft} /> : null}
<div className={styles.buttons}>
<Button
icon={<FaUndo />}
smallIcon
tooltip="Reset"
onClick={handleReset}
/>
{!running && (
<Button
icon={<FaPlay />}
smallIcon
tooltip={'Start'}
onClick={handleStart}
/>
)}
</div>
</div>
</Modal>
);
}
4 changes: 2 additions & 2 deletions src/components/toolbox/pomodoro/pomodoro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
15 changes: 0 additions & 15 deletions src/components/toolbox/pomodoro/timer/timer.tsx

This file was deleted.

0 comments on commit 71b62ed

Please sign in to comment.