Merge pull request #32 from SuperMeepBoy/add-sleep-timer

feat: add sleep timer
This commit is contained in:
MAZE ✧ 2024-04-29 00:32:09 +03:30 committed by GitHub
commit dbbd68b73d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 263 additions and 18 deletions

View file

@ -26,4 +26,9 @@
&.smallIcon {
font-size: var(--font-xsm);
}
&:disabled {
cursor: not-allowed;
opacity: 0.4;
}
}

View file

@ -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}

View file

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

View file

@ -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';

View file

@ -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}
/>
);
}

View file

@ -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);
@ -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')} />
@ -133,6 +138,10 @@ export function Menu() {
show={modals.pomodoro}
onClose={() => close('pomodoro')}
/>
<SleepTimerModal
show={modals.sleepTimer}
onClose={() => close('sleepTimer')}
/>
</>
);
}

View file

@ -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',

View file

@ -0,0 +1 @@
export { SleepTimerModal } from './sleep-timer';

View file

@ -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%;
}
}

View file

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

View file

@ -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';

View file

@ -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 (
<div className={styles.timer}>
{padNumber(Math.floor(timer / 60))}:{padNumber(timer % 60)}
</div>
);
}