Skip to content

Commit

Permalink
Merge pull request #32 from SuperMeepBoy/add-sleep-timer
Browse files Browse the repository at this point in the history
feat: add sleep timer
  • Loading branch information
remvze authored Apr 28, 2024
2 parents 0300df3 + 2e375ad commit dbbd68b
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@
&.smallIcon {
font-size: var(--font-xsm);
}

&:disabled {
cursor: not-allowed;
opacity: 0.4;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@ import { cn } from '@/helpers/styles';
import styles from './button.module.css';

interface ButtonProps {
disabled?: boolean;
icon: React.ReactElement;
onClick: () => void;
smallIcon?: boolean;
tooltip: string;
}

export function Button({ icon, onClick, smallIcon, tooltip }: ButtonProps) {
export function Button({
disabled = false,
icon,
onClick,
smallIcon,
tooltip,
}: ButtonProps) {
return (
<Tooltip content={tooltip} placement="bottom" showDelay={0}>
<button
className={cn(styles.button, smallIcon && styles.smallIcon)}
disabled={disabled}
onClick={onClick}
>
{icon}
Expand Down
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}
/>
);
}
9 changes: 9 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,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);

Expand Down Expand Up @@ -103,6 +107,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 +138,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%;
}
}
133 changes: 133 additions & 0 deletions src/components/modals/sleep-timer/sleep-timer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { useEffect, useState, useCallback, useRef } 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 = useRef<NodeJS.Timeout>();

const isPlaying = useSoundStore(state => state.isPlaying);
const play = useSoundStore(state => state.play);
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]);

const handleStart = () => {
if (timerId.current) clearInterval(timerId.current);
if (!isPlaying) play();

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);

timerId.current = newTimerId;
}
};

const handleReset = () => {
if (timerId.current) clearInterval(timerId.current);
setTimeLeft(0);
setHours('0');
setMinutes('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
disabled={calculateTotalSeconds() <= 0}
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 dbbd68b

Please sign in to comment.