mirror of
https://github.com/remvze/moodist.git
synced 2025-12-17 08:54:13 +00:00
feat: add sleep timer
This commit is contained in:
parent
60f167c4d7
commit
71b62ed3dd
15 changed files with 244 additions and 17 deletions
36
src/components/generic/timer/timer.tsx
Normal file
36
src/components/generic/timer/timer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
src/components/menu/items/sleep-timer.tsx
Normal file
18
src/components/menu/items/sleep-timer.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<Divider />
|
||||
<div className={styles.wrapper}>
|
||||
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
|
|
@ -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')} />
|
||||
|
|
@ -133,6 +139,10 @@ export function Menu() {
|
|||
show={modals.pomodoro}
|
||||
onClose={() => close('pomodoro')}
|
||||
/>
|
||||
<SleepTimerModal
|
||||
show={modals.sleepTimer}
|
||||
onClose={() => close('sleepTimer')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
1
src/components/modals/sleep-timer/index.ts
Normal file
1
src/components/modals/sleep-timer/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { SleepTimerModal } from './sleep-timer';
|
||||
45
src/components/modals/sleep-timer/sleep-timer.module.css
Normal file
45
src/components/modals/sleep-timer/sleep-timer.module.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
127
src/components/modals/sleep-timer/sleep-timer.tsx
Normal file
127
src/components/modals/sleep-timer/sleep-timer.tsx
Normal file
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue