mirror of
https://github.com/remvze/moodist.git
synced 2025-12-18 01:14:17 +00:00
Merge pull request #32 from SuperMeepBoy/add-sleep-timer
feat: add sleep timer
This commit is contained in:
commit
dbbd68b73d
15 changed files with 263 additions and 18 deletions
|
|
@ -26,4 +26,9 @@
|
||||||
&.smallIcon {
|
&.smallIcon {
|
||||||
font-size: var(--font-xsm);
|
font-size: var(--font-xsm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,17 +5,25 @@ import { cn } from '@/helpers/styles';
|
||||||
import styles from './button.module.css';
|
import styles from './button.module.css';
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
|
disabled?: boolean;
|
||||||
icon: React.ReactElement;
|
icon: React.ReactElement;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
smallIcon?: boolean;
|
smallIcon?: boolean;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button({ icon, onClick, smallIcon, tooltip }: ButtonProps) {
|
export function Button({
|
||||||
|
disabled = false,
|
||||||
|
icon,
|
||||||
|
onClick,
|
||||||
|
smallIcon,
|
||||||
|
tooltip,
|
||||||
|
}: ButtonProps) {
|
||||||
return (
|
return (
|
||||||
<Tooltip content={tooltip} placement="bottom" showDelay={0}>
|
<Tooltip content={tooltip} placement="bottom" showDelay={0}>
|
||||||
<button
|
<button
|
||||||
className={cn(styles.button, smallIcon && styles.smallIcon)}
|
className={cn(styles.button, smallIcon && styles.smallIcon)}
|
||||||
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
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);
|
||||||
|
|
||||||
|
|
@ -103,6 +107,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 +138,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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/components/modals/sleep-timer/sleep-timer.tsx
Normal file
133
src/components/modals/sleep-timer/sleep-timer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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