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 { Pomodoro as PomodoroItem } from './pomodoro';
|
||||||
export { Presets as PresetsItem } from './presets';
|
export { Presets as PresetsItem } from './presets';
|
||||||
export { Shortcuts as ShortcutsItem } from './shortcuts';
|
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,
|
PomodoroItem,
|
||||||
PresetsItem,
|
PresetsItem,
|
||||||
ShortcutsItem,
|
ShortcutsItem,
|
||||||
|
SleepTimer,
|
||||||
} from './items';
|
} from './items';
|
||||||
import { Divider } from './divider';
|
import { Divider } from './divider';
|
||||||
import { ShareLinkModal } from '@/components/modals/share-link';
|
import { ShareLinkModal } from '@/components/modals/share-link';
|
||||||
import { PresetsModal } from '@/components/modals/presets';
|
import { PresetsModal } from '@/components/modals/presets';
|
||||||
import { ShortcutsModal } from '@/components/modals/shortcuts';
|
import { ShortcutsModal } from '@/components/modals/shortcuts';
|
||||||
|
import { SleepTimerModal } from '@/components/modals/sleep-timer';
|
||||||
import { Notepad, Pomodoro } from '@/components/toolbox';
|
import { Notepad, Pomodoro } from '@/components/toolbox';
|
||||||
import { fade, mix, slideY } from '@/lib/motion';
|
import { fade, mix, slideY } from '@/lib/motion';
|
||||||
import { useSoundStore } from '@/store';
|
import { useSoundStore } from '@/store';
|
||||||
|
|
@ -37,6 +39,7 @@ export function Menu() {
|
||||||
presets: false,
|
presets: false,
|
||||||
shareLink: false,
|
shareLink: false,
|
||||||
shortcuts: false,
|
shortcuts: false,
|
||||||
|
sleepTimer: false,
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
@ -64,6 +67,7 @@ export function Menu() {
|
||||||
useHotkeys('shift+alt+p', () => open('presets'));
|
useHotkeys('shift+alt+p', () => open('presets'));
|
||||||
useHotkeys('shift+h', () => open('shortcuts'));
|
useHotkeys('shift+h', () => open('shortcuts'));
|
||||||
useHotkeys('shift+s', () => open('shareLink'), { enabled: !noSelected });
|
useHotkeys('shift+s', () => open('shareLink'), { enabled: !noSelected });
|
||||||
|
useHotkeys('shift+t', () => open('sleepTimer'));
|
||||||
|
|
||||||
useCloseListener(closeAll);
|
useCloseListener(closeAll);
|
||||||
|
|
||||||
|
|
@ -71,6 +75,7 @@ export function Menu() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Divider />
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
|
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
|
||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger asChild>
|
||||||
|
|
@ -103,6 +108,7 @@ export function Menu() {
|
||||||
<Divider />
|
<Divider />
|
||||||
<NotepadItem open={() => open('notepad')} />
|
<NotepadItem open={() => open('notepad')} />
|
||||||
<PomodoroItem open={() => open('pomodoro')} />
|
<PomodoroItem open={() => open('pomodoro')} />
|
||||||
|
<SleepTimer open={() => open('sleepTimer')} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<ShortcutsItem open={() => open('shortcuts')} />
|
<ShortcutsItem open={() => open('shortcuts')} />
|
||||||
|
|
@ -133,6 +139,10 @@ export function Menu() {
|
||||||
show={modals.pomodoro}
|
show={modals.pomodoro}
|
||||||
onClose={() => close('pomodoro')}
|
onClose={() => close('pomodoro')}
|
||||||
/>
|
/>
|
||||||
|
<SleepTimerModal
|
||||||
|
show={modals.sleepTimer}
|
||||||
|
onClose={() => close('sleepTimer')}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
|
||||||
keys: ['Shift', 'P'],
|
keys: ['Shift', 'P'],
|
||||||
label: 'Pomodoro Timer',
|
label: 'Pomodoro Timer',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
keys: ['Shift', 'T'],
|
||||||
|
label: 'Sleep Timer',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
keys: ['Shift', 'Space'],
|
keys: ['Shift', 'Space'],
|
||||||
label: 'Toggle Play',
|
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 { IoMdSettings } from 'react-icons/io/index';
|
||||||
|
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
|
import { Button } from '@/components/generic/button';
|
||||||
|
import { Timer } from '@/components/generic/timer';
|
||||||
import { Tabs } from './tabs';
|
import { Tabs } from './tabs';
|
||||||
import { Timer } from './timer';
|
|
||||||
import { Button } from './button';
|
|
||||||
import { Setting } from './setting';
|
import { Setting } from './setting';
|
||||||
|
|
||||||
import { useLocalStorage } from '@/hooks/use-local-storage';
|
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