diff --git a/src/components/menu/items/breathing-exercise.tsx b/src/components/menu/items/breathing-exercise.tsx deleted file mode 100644 index 5695afb..0000000 --- a/src/components/menu/items/breathing-exercise.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { IoMdFlower } from 'react-icons/io/index'; - -import { Item } from '../item'; - -interface BreathingExerciseProps { - open: () => void; -} - -export function BreathingExercise({ open }: BreathingExerciseProps) { - return ( - } - label="Breathing Exercise" - shortcut="Shift + B" - onClick={open} - /> - ); -} diff --git a/src/components/menu/items/countdown.tsx b/src/components/menu/items/countdown.tsx deleted file mode 100644 index cd3b1a6..0000000 --- a/src/components/menu/items/countdown.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { MdOutlineTimer } from 'react-icons/md/index'; - -import { Item } from '../item'; - -interface CountdownProps { - open: () => void; -} - -export function Countdown({ open }: CountdownProps) { - return ( - } - label="Countdown Timer" - shortcut="Shift + C" - onClick={open} - /> - ); -} diff --git a/src/components/menu/items/index.ts b/src/components/menu/items/index.ts index ed49f67..dda2e17 100644 --- a/src/components/menu/items/index.ts +++ b/src/components/menu/items/index.ts @@ -5,8 +5,3 @@ export { Source as SourceItem } from './source'; export { Presets as PresetsItem } from './presets'; export { Shortcuts as ShortcutsItem } from './shortcuts'; export { SleepTimer as SleepTimerItem } from './sleep-timer'; -export { Notepad as NotepadItem } from './notepad'; -export { Pomodoro as PomodoroItem } from './pomodoro'; -export { Countdown as CountdownItem } from './countdown'; -export { BreathingExercise as BreathingExerciseItem } from './breathing-exercise'; -export { Todo as TodoItem } from './todo'; diff --git a/src/components/menu/items/notepad.tsx b/src/components/menu/items/notepad.tsx deleted file mode 100644 index ef0ff39..0000000 --- a/src/components/menu/items/notepad.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { MdNotes } from 'react-icons/md/index'; - -import { Item } from '../item'; - -import { useNoteStore } from '@/stores/note'; - -interface NotepadProps { - open: () => void; -} - -export function Notepad({ open }: NotepadProps) { - const note = useNoteStore(state => state.note); - - return ( - } - label="Notepad" - shortcut="Shift + N" - onClick={open} - /> - ); -} diff --git a/src/components/menu/items/pomodoro.tsx b/src/components/menu/items/pomodoro.tsx deleted file mode 100644 index 43473b5..0000000 --- a/src/components/menu/items/pomodoro.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { MdOutlineAvTimer } from 'react-icons/md/index'; - -import { Item } from '../item'; - -import { usePomodoroStore } from '@/stores/pomodoro'; - -interface PomodoroProps { - open: () => void; -} - -export function Pomodoro({ open }: PomodoroProps) { - const running = usePomodoroStore(state => state.running); - - return ( - } - label="Pomodoro" - shortcut="Shift + P" - onClick={open} - /> - ); -} diff --git a/src/components/menu/items/todo.tsx b/src/components/menu/items/todo.tsx deleted file mode 100644 index f2dbc1e..0000000 --- a/src/components/menu/items/todo.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { MdTaskAlt } from 'react-icons/md/index'; - -import { Item } from '../item'; - -interface TodoProps { - open: () => void; -} - -export function Todo({ open }: TodoProps) { - return ( - } - label="Todo Checklist" - shortcut="Shift + T" - onClick={open} - /> - ); -} diff --git a/src/components/menu/menu.tsx b/src/components/menu/menu.tsx index 71325d6..61ab098 100644 --- a/src/components/menu/menu.tsx +++ b/src/components/menu/menu.tsx @@ -12,19 +12,12 @@ import { PresetsItem, ShortcutsItem, SleepTimerItem, - NotepadItem, - PomodoroItem, - CountdownItem, - BreathingExerciseItem, - TodoItem, } 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 { BreathingExerciseModal } from '../modals/breathing'; -import { Notepad, Countdown, Pomodoro, Todo } from '../toolbox'; import { fade, mix, slideY } from '@/lib/motion'; import { useSoundStore } from '@/stores/sound'; @@ -117,13 +110,6 @@ export function Menu() { open('sleepTimer')} /> - - open('countdown')} /> - open('pomodoro')} /> - open('notepad')} /> - open('todo')} /> - open('breathing')} /> - open('shortcuts')} /> @@ -146,18 +132,6 @@ export function Menu() { show={modals.shortcuts} onClose={() => close('shortcuts')} /> - close('breathing')} - /> - close('notepad')} /> - close('todo')} /> - close('countdown')} /> - open('pomodoro')} - show={modals.pomodoro} - onClose={() => close('pomodoro')} - /> close('presets')} /> void; - show: boolean; -} - -export function BreathingExerciseModal({ onClose, show }: TimerProps) { - return ( - - Breathing Exercise - - - ); -} diff --git a/src/components/modals/breathing/exercise/exercise.module.css b/src/components/modals/breathing/exercise/exercise.module.css deleted file mode 100644 index 437810f..0000000 --- a/src/components/modals/breathing/exercise/exercise.module.css +++ /dev/null @@ -1,91 +0,0 @@ -.exercise { - position: relative; - z-index: 1; - display: flex; - align-items: center; - justify-content: center; - padding: 75px 0; - margin-top: 12px; - background-color: var(--color-neutral-50); - border: 1px solid var(--color-neutral-200); - border-radius: 8px; - - & .timer { - position: absolute; - top: 4px; - left: 4px; - padding: 4px 12px; - font-size: var(--font-xsm); - color: var(--color-foreground-subtle); - background: linear-gradient( - var(--color-neutral-100), - var(--color-neutral-50) - ); - border: 1px solid var(--color-neutral-200); - border-radius: 4px; - } - - & .phase { - font-family: var(--font-display); - font-size: var(--font-lg); - font-weight: 600; - } - - & .circle { - position: absolute; - top: 50%; - left: 50%; - z-index: -1; - height: 55%; - aspect-ratio: 1 / 1; - background-image: radial-gradient( - var(--color-neutral-50), - var(--color-neutral-100) - ); - border: 1px solid var(--color-neutral-200); - border-radius: 50%; - transform: translate(-50%, -50%); - } -} - -.selectWrapper { - position: relative; - width: 100%; - height: 45px; - padding: 0 12px; - margin-top: 8px; - background-color: var(--color-neutral-50); - border: 1px solid var(--color-neutral-200); - border-radius: 8px; - - &::before { - position: absolute; - top: -1px; - left: 50%; - width: 80%; - height: 1px; - content: ''; - background: linear-gradient( - 90deg, - transparent, - var(--color-neutral-300), - transparent - ); - transform: translateX(-50%); - } - - & .selectBox { - width: 100%; - min-width: 0; - height: 100%; - font-size: var(--font-sm); - color: var(--color-foreground); - background-color: transparent; - border: none; - outline: none; - - & option { - color: var(--color-neutral-50); - } - } -} diff --git a/src/components/modals/breathing/exercise/exercise.tsx b/src/components/modals/breathing/exercise/exercise.tsx deleted file mode 100644 index 3920b27..0000000 --- a/src/components/modals/breathing/exercise/exercise.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useState, useEffect, useMemo, useCallback } from 'react'; -import { motion } from 'framer-motion'; - -import { padNumber } from '@/helpers/number'; - -import styles from './exercise.module.css'; - -type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing'; -type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale'; - -const EXERCISE_PHASES: Record = { - '4-7-8 Breathing': ['inhale', 'holdInhale', 'exhale'], - 'Box Breathing': ['inhale', 'holdInhale', 'exhale', 'holdExhale'], - 'Resonant Breathing': ['inhale', 'exhale'], -}; - -const EXERCISE_DURATIONS: Record>> = { - '4-7-8 Breathing': { exhale: 8, holdInhale: 7, inhale: 4 }, - 'Box Breathing': { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 }, - 'Resonant Breathing': { exhale: 5, inhale: 5 }, // No holdExhale -}; - -const PHASE_LABELS: Record = { - exhale: 'Exhale', - holdExhale: 'Hold', - holdInhale: 'Hold', - inhale: 'Inhale', -}; - -export function Exercise() { - const [selectedExercise, setSelectedExercise] = - useState('4-7-8 Breathing'); - const [phaseIndex, setPhaseIndex] = useState(0); - - const phases = useMemo( - () => EXERCISE_PHASES[selectedExercise], - [selectedExercise], - ); - const durations = useMemo( - () => EXERCISE_DURATIONS[selectedExercise], - [selectedExercise], - ); - - const currentPhase = phases[phaseIndex]; - - const animationVariants = useMemo( - () => ({ - exhale: { - transform: 'translate(-50%, -50%) scale(1)', - transition: { duration: durations.exhale }, - }, - holdExhale: { - transform: 'translate(-50%, -50%) scale(1)', - transition: { duration: durations.holdExhale }, - }, - holdInhale: { - transform: 'translate(-50%, -50%) scale(1.5)', - transition: { duration: durations.holdInhale }, - }, - inhale: { - transform: 'translate(-50%, -50%) scale(1.5)', - transition: { duration: durations.inhale }, - }, - }), - [durations], - ); - - const resetExercise = useCallback(() => { - setPhaseIndex(0); - }, []); - - const updatePhase = useCallback(() => { - setPhaseIndex(prevIndex => (prevIndex + 1) % phases.length); - }, [phases.length]); - - useEffect(() => { - resetExercise(); - }, [selectedExercise, resetExercise]); - - useEffect(() => { - const intervalDuration = (durations[currentPhase] || 4) * 1000; - const interval = setInterval(updatePhase, intervalDuration); - - return () => clearInterval(interval); - }, [currentPhase, durations, updatePhase]); - - const [timer, setTimer] = useState(0); - - useEffect(() => { - const interval = setInterval(() => setTimer(prev => prev + 1), 1000); - - return () => clearInterval(interval); - }, []); - - return ( - <> - - - {padNumber(Math.floor(timer / 60))}:{padNumber(timer % 60)} - - - - {PHASE_LABELS[currentPhase]} - - - - setSelectedExercise(e.target.value as Exercise)} - > - {Object.keys(EXERCISE_PHASES).map(exercise => ( - - {exercise} - - ))} - - - > - ); -} diff --git a/src/components/modals/breathing/exercise/index.ts b/src/components/modals/breathing/exercise/index.ts deleted file mode 100644 index 881062d..0000000 --- a/src/components/modals/breathing/exercise/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Exercise } from './exercise'; diff --git a/src/components/modals/breathing/index.ts b/src/components/modals/breathing/index.ts deleted file mode 100644 index 1433198..0000000 --- a/src/components/modals/breathing/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BreathingExerciseModal } from './breathing'; diff --git a/src/components/modals/shortcuts/shortcuts.tsx b/src/components/modals/shortcuts/shortcuts.tsx index 0a8b57a..ff2cee6 100644 --- a/src/components/modals/shortcuts/shortcuts.tsx +++ b/src/components/modals/shortcuts/shortcuts.tsx @@ -25,10 +25,6 @@ export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) { keys: ['Shift', 'T'], label: 'Sleep Timer', }, - { - keys: ['Shift', 'B'], - label: 'Breathing Exercise', - }, { keys: ['Shift', 'Space'], label: 'Toggle Play', diff --git a/src/components/store-consumer/store-consumer.tsx b/src/components/store-consumer/store-consumer.tsx index 101be03..f3f5f0e 100644 --- a/src/components/store-consumer/store-consumer.tsx +++ b/src/components/store-consumer/store-consumer.tsx @@ -1,7 +1,6 @@ import { useEffect } from 'react'; import { useSoundStore } from '@/stores/sound'; -import { useNoteStore } from '@/stores/note'; import { usePresetStore } from '@/stores/preset'; interface StoreConsumerProps { @@ -11,7 +10,6 @@ interface StoreConsumerProps { export function StoreConsumer({ children }: StoreConsumerProps) { useEffect(() => { useSoundStore.persist.rehydrate(); - useNoteStore.persist.rehydrate(); usePresetStore.persist.rehydrate(); }, []); diff --git a/src/components/toolbox/countdown/countdown.module.css b/src/components/toolbox/countdown/countdown.module.css deleted file mode 100644 index 780bc50..0000000 --- a/src/components/toolbox/countdown/countdown.module.css +++ /dev/null @@ -1,128 +0,0 @@ -.header { - margin-bottom: 16px; - - & .title { - margin-bottom: 8px; - font-family: var(--font-heading); - font-size: var(--font-md); - font-weight: 600; - } - - & .desc { - color: var(--color-foreground-subtle); - } -} - -.formContainer { - & .inputContainer { - display: flex; - column-gap: 8px; - align-items: center; - - & .input { - display: block; - flex-grow: 1; - min-width: 0; - height: 45px; - padding: 0 8px; - color: var(--color-foreground); - background-color: var(--color-neutral-50); - border: 1px solid var(--color-neutral-200); - border-radius: 8px; - } - - & span { - display: block; - } - } -} - -.displayTime { - position: relative; - display: flex; - align-items: center; - justify-content: center; - height: 150px; - background-color: var(--color-neutral-50); - border: 1px solid var(--color-neutral-200); - border-radius: 8px; - - &::before { - position: absolute; - bottom: -1px; - left: 50%; - width: 80%; - height: 1px; - content: ''; - background: linear-gradient( - 90deg, - transparent, - var(--color-neutral-300), - transparent - ); - transform: translateX(-50%); - } - - & span { - font-size: var(--font-xlg); - font-weight: 600; - } - - & .reverse { - position: absolute; - top: 4px; - left: 4px; - padding: 4px 12px; - font-size: var(--font-xsm); - color: var(--color-foreground-subtle); - background: linear-gradient( - var(--color-neutral-100), - var(--color-neutral-50) - ); - border: 1px solid var(--color-neutral-200); - border-radius: 4px; - } -} - -.buttonContainer { - display: flex; - column-gap: 8px; - align-items: center; - justify-content: flex-end; - margin-top: 12px; - - & .button { - padding: 12px 16px; - font-family: var(--font-heading); - font-size: var(--font-sm); - font-weight: 600; - color: var(--color-foreground-subtle); - cursor: pointer; - background-color: var(--color-neutral-200); - border: none; - border-radius: 4px; - outline: none; - transition: 0.2s; - - &:focus-visible { - outline: 2px solid var(--color-neutral-400); - outline-offset: 2px; - } - - &:hover, - &:focus-visible { - color: var(--color-foreground); - background-color: var(--color-neutral-300); - } - - &.primary { - color: var(--color-neutral-200); - background-color: var(--color-neutral-950); - - &:hover, - &:focus-visible { - background-color: var(--color-neutral-800); - } - } - } -} diff --git a/src/components/toolbox/countdown/countdown.tsx b/src/components/toolbox/countdown/countdown.tsx deleted file mode 100644 index 6a5ca1b..0000000 --- a/src/components/toolbox/countdown/countdown.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; - -import { Modal } from '@/components/modal'; - -import { useSoundEffect } from '@/hooks/use-sound-effect'; -import { cn } from '@/helpers/styles'; -import { padNumber } from '@/helpers/number'; - -import styles from './countdown.module.css'; - -interface CountdownProps { - onClose: () => void; - show: boolean; -} - -export function Countdown({ onClose, show }: CountdownProps) { - const [hours, setHours] = useState(0); - const [minutes, setMinutes] = useState(0); - const [seconds, setSeconds] = useState(0); - const [timeLeft, setTimeLeft] = useState(0); - const [initialTime, setInitialTime] = useState(0); - const [isActive, setIsActive] = useState(false); - const [isFormVisible, setIsFormVisible] = useState(true); - - const alarm = useSoundEffect('/sounds/alarm.mp3'); - - useEffect(() => { - let timer: NodeJS.Timeout; - - if (isActive && timeLeft > 0) { - timer = setTimeout(() => setTimeLeft(timeLeft - 1), 1000); - } else if (timeLeft === 0 && isActive) { - alarm.play(); - setIsActive(false); - setIsFormVisible(true); - } - - return () => clearTimeout(timer); - }, [isActive, timeLeft, alarm]); - - const handleStart = useCallback(() => { - if (hours > 0 || minutes > 0 || seconds > 0) { - const totalTime = - (hours || 0) * 3600 + (minutes || 0) * 60 + (seconds || 0); - - setTimeLeft(totalTime); - setInitialTime(totalTime); - setIsActive(true); - setIsFormVisible(false); - } - }, [hours, minutes, seconds]); - - const handleBack = useCallback(() => { - setIsActive(false); - setIsFormVisible(true); - setTimeLeft(0); - }, []); - - const toggleTimer = useCallback(() => { - setIsActive(prev => !prev); - }, []); - - const formatTime = useCallback((time: number) => { - const hrs = Math.floor(time / 3600); - const mins = Math.floor((time % 3600) / 60); - const secs = time % 60; - - return `${padNumber(hrs)}:${padNumber(mins)}:${padNumber(secs)}`; - }, []); - - const elapsedTime = initialTime - timeLeft; - - return ( - - - Countdown Timer - Super simple countdown timer. - - - {isFormVisible ? ( - - - setHours(Math.max(0, parseInt(e.target.value)))} - /> - - : - - - setMinutes(Math.max(0, Math.min(59, parseInt(e.target.value)))) - } - /> - - : - - - setSeconds(Math.max(0, Math.min(59, parseInt(e.target.value)))) - } - /> - - - - - Start - - - - ) : ( - - - - {formatTime(elapsedTime)} - {formatTime(timeLeft)} - - - - - Back - - - - {isActive ? 'Pause' : 'Start'} - - - - )} - - ); -} diff --git a/src/components/toolbox/countdown/index.ts b/src/components/toolbox/countdown/index.ts deleted file mode 100644 index a7731c7..0000000 --- a/src/components/toolbox/countdown/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Countdown } from './countdown'; diff --git a/src/components/toolbox/generics/button/button.module.css b/src/components/toolbox/generics/button/button.module.css deleted file mode 100644 index a96e112..0000000 --- a/src/components/toolbox/generics/button/button.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.button { - display: flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - font-size: var(--font-sm); - color: var(--color-foreground); - cursor: pointer; - background-color: var(--color-neutral-100); - border: 1px solid var(--color-neutral-200); - border-radius: 4px; - outline: none; - transition: 0.2s; - - &:focus-visible { - outline: 2px solid var(--color-neutral-400); - outline-offset: 2px; - } - - &:hover, - &:focus-visible { - background-color: var(--color-neutral-200); - } - - &.smallIcon { - font-size: var(--font-xsm); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.4; - } -} diff --git a/src/components/toolbox/generics/button/button.tsx b/src/components/toolbox/generics/button/button.tsx deleted file mode 100644 index 5828ad6..0000000 --- a/src/components/toolbox/generics/button/button.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Tooltip } from '@/components/tooltip'; - -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({ - disabled = false, - icon, - onClick, - smallIcon, - tooltip, -}: ButtonProps) { - return ( - - - {icon} - - - ); -} diff --git a/src/components/toolbox/generics/button/index.ts b/src/components/toolbox/generics/button/index.ts deleted file mode 100644 index a039b75..0000000 --- a/src/components/toolbox/generics/button/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Button } from './button'; diff --git a/src/components/toolbox/index.ts b/src/components/toolbox/index.ts deleted file mode 100644 index 0b100ca..0000000 --- a/src/components/toolbox/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { Notepad } from './notepad'; -export { Pomodoro } from './pomodoro'; -export { Todo } from './todo'; -export { Countdown } from './countdown'; diff --git a/src/components/toolbox/notepad/button/button.module.css b/src/components/toolbox/notepad/button/button.module.css deleted file mode 100644 index 4a4e756..0000000 --- a/src/components/toolbox/notepad/button/button.module.css +++ /dev/null @@ -1,45 +0,0 @@ -.button { - display: flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - font-size: var(--font-sm); - color: var(--color-foreground); - cursor: pointer; - background-color: var(--color-neutral-100); - border: 1px solid var(--color-neutral-200); - border-radius: 4px; - outline: none; - transition: 0.2s; - transition-property: border-color, color, background-color; - - &:focus-visible { - outline: 2px solid var(--color-neutral-400); - outline-offset: 2px; - } - - &.critical { - color: #f43f5e; - border-color: #f43f5e; - - &:hover { - background-color: rgb(244 63 94 / 10%); - } - } - - &.recommended { - font-size: var(--font-xsm); - color: #22c55e; - border-color: #22c55e; - - &:hover { - background-color: rgb(34 197 94 / 10%); - } - } - - &:hover, - &:focus-visible { - background-color: var(--color-neutral-200); - } -} diff --git a/src/components/toolbox/notepad/button/button.tsx b/src/components/toolbox/notepad/button/button.tsx deleted file mode 100644 index d3da910..0000000 --- a/src/components/toolbox/notepad/button/button.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Tooltip } from '@/components/tooltip'; - -import { cn } from '@/helpers/styles'; - -import styles from './button.module.css'; - -interface ButtonProps { - critical?: boolean; - icon: React.ReactElement; - onClick: () => void; - recommended?: boolean; - tooltip: string; -} - -export function Button({ - critical, - icon, - onClick, - recommended, - tooltip, -}: ButtonProps) { - return ( - - - {icon} - - - ); -} diff --git a/src/components/toolbox/notepad/button/index.ts b/src/components/toolbox/notepad/button/index.ts deleted file mode 100644 index a039b75..0000000 --- a/src/components/toolbox/notepad/button/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Button } from './button'; diff --git a/src/components/toolbox/notepad/index.ts b/src/components/toolbox/notepad/index.ts deleted file mode 100644 index 8a3fad5..0000000 --- a/src/components/toolbox/notepad/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Notepad } from './notepad'; diff --git a/src/components/toolbox/notepad/notepad.module.css b/src/components/toolbox/notepad/notepad.module.css deleted file mode 100644 index 2dadc40..0000000 --- a/src/components/toolbox/notepad/notepad.module.css +++ /dev/null @@ -1,44 +0,0 @@ -.header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; - - & .label { - font-size: var(--font-sm); - font-weight: 500; - color: var(--color-foreground-subtle); - } - - & .buttons { - display: flex; - column-gap: 4px; - align-items: center; - } -} - -.textarea { - width: 100%; - height: 350px; - padding: 12px; - line-height: 1.6; - color: var(--color-foreground-subtle); - resize: none; - background-color: var(--color-neutral-50); - border: 1px solid var(--color-neutral-200); - border-radius: 4px; - outline: none; - scroll-padding-bottom: 12px; - - &:focus-visible { - outline: 2px solid var(--color-neutral-400); - outline-offset: 2px; - } -} - -.counter { - margin-top: 8px; - font-size: var(--font-xsm); - color: var(--color-foreground-subtle); - text-align: center; -} diff --git a/src/components/toolbox/notepad/notepad.tsx b/src/components/toolbox/notepad/notepad.tsx deleted file mode 100644 index 921556f..0000000 --- a/src/components/toolbox/notepad/notepad.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useRef, useEffect } from 'react'; -import { BiTrash } from 'react-icons/bi/index'; -import { LuCopy, LuDownload } from 'react-icons/lu/index'; -import { FaCheck } from 'react-icons/fa6/index'; -import { FaUndo } from 'react-icons/fa/index'; - -import { Modal } from '@/components/modal'; -import { Button } from './button'; - -import { useNoteStore } from '@/stores/note'; -import { useCopy } from '@/hooks/use-copy'; -import { download } from '@/helpers/download'; - -import styles from './notepad.module.css'; - -interface NotepadProps { - onClose: () => void; - show: boolean; -} - -export function Notepad({ onClose, show }: NotepadProps) { - const textareaRef = useRef(null); - - const note = useNoteStore(state => state.note); - const history = useNoteStore(state => state.history); - const write = useNoteStore(state => state.write); - const words = useNoteStore(state => state.words()); - const characters = useNoteStore(state => state.characters()); - const clear = useNoteStore(state => state.clear); - const restore = useNoteStore(state => state.restore); - - const { copy, copying } = useCopy(); - - useEffect(() => { - if (show && textareaRef.current) { - setTimeout(() => { - textareaRef.current?.focus(); - }, 10); - } - }, [show]); - - const handleKeyDown = (e: React.KeyboardEvent) => { - e.stopPropagation(); - - if (e.key === 'Escape') onClose(); - }; - - return ( - - - Your Note - - : } - tooltip="Copy Note" - onClick={() => copy(note)} - /> - } - tooltip="Download Note" - onClick={() => download('Moodit Note.txt', note)} - /> - : } - recommended={!!history} - tooltip={history ? 'Restore Note' : 'Clear Note'} - onClick={() => (history ? restore() : clear())} - /> - - - - write(e.target.value)} - onKeyDown={handleKeyDown} - /> - - - {characters} character{characters !== 1 && 's'} • {words} word - {words !== 1 && 's'} - - - ); -} diff --git a/src/components/toolbox/pomodoro/index.ts b/src/components/toolbox/pomodoro/index.ts deleted file mode 100644 index 9b721ae..0000000 --- a/src/components/toolbox/pomodoro/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Pomodoro } from './pomodoro'; diff --git a/src/components/toolbox/pomodoro/pomodoro.module.css b/src/components/toolbox/pomodoro/pomodoro.module.css deleted file mode 100644 index 33109cf..0000000 --- a/src/components/toolbox/pomodoro/pomodoro.module.css +++ /dev/null @@ -1,36 +0,0 @@ -.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); - } - - & .buttons { - display: flex; - column-gap: 4px; - align-items: center; - } -} - -.control { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 8px; - - & .completed { - font-size: var(--font-xsm); - color: var(--color-foreground-subtle); - } - - & .buttons { - display: flex; - column-gap: 4px; - align-items: center; - } -} diff --git a/src/components/toolbox/pomodoro/pomodoro.tsx b/src/components/toolbox/pomodoro/pomodoro.tsx deleted file mode 100644 index 3f612d6..0000000 --- a/src/components/toolbox/pomodoro/pomodoro.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; -import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index'; -import { IoMdSettings } from 'react-icons/io/index'; - -import { Modal } from '@/components/modal'; -import { Button } from '../generics/button'; -import { Timer } from '@/components/timer'; -import { Tabs } from './tabs'; -import { Setting } from './setting'; - -import { useLocalStorage } from '@/hooks/use-local-storage'; -import { useSoundEffect } from '@/hooks/use-sound-effect'; -import { usePomodoroStore } from '@/stores/pomodoro'; -import { useCloseListener } from '@/hooks/use-close-listener'; - -import styles from './pomodoro.module.css'; - -interface PomodoroProps { - onClose: () => void; - open: () => void; - show: boolean; -} - -export function Pomodoro({ onClose, open, show }: PomodoroProps) { - const [showSetting, setShowSetting] = useState(false); - - const [selectedTab, setSelectedTab] = useState('pomodoro'); - - const running = usePomodoroStore(state => state.running); - const setRunning = usePomodoroStore(state => state.setRunning); - - const [timer, setTimer] = useState(0); - const interval = useRef | null>(null); - - const alarm = useSoundEffect('/sounds/alarm.mp3'); - - const defaultTimes = useMemo( - () => ({ - long: 15 * 60, - pomodoro: 25 * 60, - short: 5 * 60, - }), - [], - ); - - const [times, setTimes] = useLocalStorage>( - 'moodist-pomodoro-setting', - defaultTimes, - ); - - const [completions, setCompletions] = useState>({ - long: 0, - pomodoro: 0, - short: 0, - }); - - const tabs = useMemo( - () => [ - { id: 'pomodoro', label: 'Pomodoro' }, - { id: 'short', label: 'Break' }, - { id: 'long', label: 'Long Break' }, - ], - [], - ); - - useCloseListener(() => setShowSetting(false)); - - useEffect(() => { - if (running) { - if (interval.current) clearInterval(interval.current); - - interval.current = setInterval(() => { - setTimer(prev => prev - 1); - }, 1000); - } else { - if (interval.current) clearInterval(interval.current); - } - }, [running]); - - useEffect(() => { - if (timer <= 0 && running) { - if (interval.current) clearInterval(interval.current); - - alarm.play(); - - setRunning(false); - setCompletions(prev => ({ - ...prev, - [selectedTab]: prev[selectedTab] + 1, - })); - } - }, [timer, selectedTab, running, setRunning, alarm]); - - useEffect(() => { - const time = times[selectedTab] || 10; - - if (interval.current) clearInterval(interval.current); - - setRunning(false); - setTimer(time); - }, [selectedTab, times, setRunning]); - - const toggleRunning = () => { - if (running) setRunning(false); - else if (timer <= 0) { - const time = times[selectedTab] || 10; - - setTimer(time); - setRunning(true); - } else setRunning(true); - }; - - const restart = () => { - if (interval.current) clearInterval(interval.current); - - const time = times[selectedTab] || 10; - - setRunning(false); - setTimer(time); - }; - - return ( - <> - - - Pomodoro Timer - - - } - tooltip="Change Times" - onClick={() => { - onClose(); - setShowSetting(true); - }} - /> - - - - - - - - - {completions[selectedTab] || 0} completed - - - } - smallIcon - tooltip="Restart" - onClick={restart} - /> - : } - smallIcon - tooltip={running ? 'Pause' : 'Start'} - onClick={toggleRunning} - /> - - - - - { - setShowSetting(false); - setTimes(times); - open(); - }} - onClose={() => { - setShowSetting(false); - open(); - }} - /> - > - ); -} diff --git a/src/components/toolbox/pomodoro/setting/index.ts b/src/components/toolbox/pomodoro/setting/index.ts deleted file mode 100644 index 0394f8b..0000000 --- a/src/components/toolbox/pomodoro/setting/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Setting } from './setting'; diff --git a/src/components/toolbox/pomodoro/setting/setting.module.css b/src/components/toolbox/pomodoro/setting/setting.module.css deleted file mode 100644 index 89583ef..0000000 --- a/src/components/toolbox/pomodoro/setting/setting.module.css +++ /dev/null @@ -1,76 +0,0 @@ -.title { - margin-bottom: 16px; - font-family: var(--font-heading); - font-size: var(--font-md); - font-weight: 600; -} - -& .form { - display: flex; - flex-direction: column; - - & .field { - display: flex; - flex-direction: column; - row-gap: 8px; - margin-bottom: 16px; - - & .label { - font-size: var(--font-sm); - color: var(--color-foreground); - - & span { - color: var(--color-foreground-subtle); - } - } - - & .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; - - &:focus-visible { - outline: 2px solid var(--color-neutral-400); - outline-offset: 2px; - } - } - } - - & .buttons { - display: flex; - column-gap: 8px; - align-items: center; - justify-content: flex-end; - - & button { - display: flex; - align-items: center; - justify-content: center; - height: 40px; - padding: 0 16px; - font-size: var(--font-sm); - font-weight: 500; - color: var(--color-foreground); - cursor: pointer; - background-color: var(--color-neutral-200); - border: none; - border-radius: 4px; - outline: none; - - &:focus-visible { - outline: 2px solid var(--color-neutral-400); - outline-offset: 2px; - } - - &.primary { - color: var(--color-neutral-100); - background-color: var(--color-neutral-950); - } - } - } -} diff --git a/src/components/toolbox/pomodoro/setting/setting.tsx b/src/components/toolbox/pomodoro/setting/setting.tsx deleted file mode 100644 index 8db1a93..0000000 --- a/src/components/toolbox/pomodoro/setting/setting.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { Modal } from '@/components/modal'; - -import styles from './setting.module.css'; - -interface SettingProps { - onChange: (newTimes: Record) => void; - onClose: () => void; - show: boolean; - times: Record; -} - -export function Setting({ onChange, onClose, show, times }: SettingProps) { - const [values, setValues] = useState>(times); - - useEffect(() => { - if (show) setValues(times); - }, [times, show]); - - const handleChange = (id: string) => (value: number | string) => { - setValues(prev => ({ - ...prev, - [id]: typeof value === 'number' ? value * 60 : '', - })); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const newValues: Record = {}; - - Object.keys(values).forEach(name => { - newValues[name] = - typeof values[name] === 'number' ? values[name] : times[name]; - }); - - onChange(newValues); - }; - - const handleCancel = (e: React.MouseEvent) => { - e.preventDefault(); - - onClose(); - }; - - return ( - - Change Times - - - - - - - - - Cancel - - - Save - - - - - ); -} - -interface FieldProps { - id: string; - label: string; - onChange: (value: number | string) => void; - value: number | string; -} - -function Field({ id, label, onChange, value }: FieldProps) { - return ( - - - {label} (minutes) - - { - onChange(e.target.value === '' ? '' : Number(e.target.value)); - }} - /> - - ); -} diff --git a/src/components/toolbox/pomodoro/tabs/index.ts b/src/components/toolbox/pomodoro/tabs/index.ts deleted file mode 100644 index 81aabb7..0000000 --- a/src/components/toolbox/pomodoro/tabs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Tabs } from './tabs'; diff --git a/src/components/toolbox/pomodoro/tabs/tabs.module.css b/src/components/toolbox/pomodoro/tabs/tabs.module.css deleted file mode 100644 index 222164c..0000000 --- a/src/components/toolbox/pomodoro/tabs/tabs.module.css +++ /dev/null @@ -1,43 +0,0 @@ -.tabs { - display: flex; - column-gap: 4px; - align-items: center; - padding: 4px; - margin: 8px 0; - background-color: var(--color-neutral-50); - border: 1px solid var(--color-neutral-200); - border-radius: 8px; - - & .tab { - display: flex; - flex-grow: 1; - align-items: center; - justify-content: center; - height: 45px; - font-size: var(--font-sm); - color: var(--color-foreground-subtle); - cursor: pointer; - background-color: transparent; - border: 1px solid transparent; - border-radius: 4px; - outline: none; - transition: 0.2s; - - &:focus-visible { - outline: 2px solid var(--color-neutral-400); - outline-offset: 2px; - } - - &.selected { - color: var(--color-foreground); - background-color: var(--color-neutral-200); - border-color: var(--color-neutral-300); - } - - &:not(.selected):hover, - &:not(.selected):focus-visible { - color: var(--color-foreground); - background-color: var(--color-neutral-100); - } - } -} diff --git a/src/components/toolbox/pomodoro/tabs/tabs.tsx b/src/components/toolbox/pomodoro/tabs/tabs.tsx deleted file mode 100644 index 728bc5e..0000000 --- a/src/components/toolbox/pomodoro/tabs/tabs.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { cn } from '@/helpers/styles'; - -import styles from './tabs.module.css'; - -interface TabsProps { - onSelect: (id: string) => void; - selectedTab: string; - tabs: Array<{ id: string; label: string }>; -} - -export function Tabs({ onSelect, selectedTab, tabs }: TabsProps) { - return ( - - {tabs.map(tab => ( - onSelect(tab.id)} - > - {tab.label} - - ))} - - ); -} diff --git a/src/components/toolbox/todo/form/form.module.css b/src/components/toolbox/todo/form/form.module.css deleted file mode 100644 index e48d56f..0000000 --- a/src/components/toolbox/todo/form/form.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.wrapper { - display: flex; - align-items: center; - height: 45px; - padding: 4px; - margin-top: 12px; - background-color: var(--color-neutral-50); - border: 1px solid var(--color-neutral-200); - border-radius: 8px; - - & input { - flex-grow: 1; - min-width: 0; - height: 100%; - padding: 0 8px; - color: var(--color-foreground); - background-color: transparent; - border: none; - outline: none; - } - - & button { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - padding: 0 16px; - font-size: var(--font-sm); - color: var(--color-foreground); - cursor: pointer; - background-color: var(--color-neutral-100); - border: 1px solid var(--color-neutral-200); - border-radius: 4px; - } -} diff --git a/src/components/toolbox/todo/form/form.tsx b/src/components/toolbox/todo/form/form.tsx deleted file mode 100644 index 853f04c..0000000 --- a/src/components/toolbox/todo/form/form.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useState } from 'react'; - -import { useTodoStore } from '@/stores/todo'; - -import styles from './form.module.css'; - -export function Form() { - const [value, setValue] = useState(''); - - const addTodo = useTodoStore(state => state.addTodo); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (!value.trim().length) return; - - addTodo(value); - setValue(''); - }; - - return ( - - - setValue(e.target.value)} - /> - Add - - - ); -} diff --git a/src/components/toolbox/todo/form/index.ts b/src/components/toolbox/todo/form/index.ts deleted file mode 100644 index 9398c9e..0000000 --- a/src/components/toolbox/todo/form/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Form } from './form'; diff --git a/src/components/toolbox/todo/index.ts b/src/components/toolbox/todo/index.ts deleted file mode 100644 index b54a84e..0000000 --- a/src/components/toolbox/todo/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Todo } from './todo'; diff --git a/src/components/toolbox/todo/todo.module.css b/src/components/toolbox/todo/todo.module.css deleted file mode 100644 index fdbd99d..0000000 --- a/src/components/toolbox/todo/todo.module.css +++ /dev/null @@ -1 +0,0 @@ -/* WIP */ diff --git a/src/components/toolbox/todo/todo.tsx b/src/components/toolbox/todo/todo.tsx deleted file mode 100644 index 05663cd..0000000 --- a/src/components/toolbox/todo/todo.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Modal } from '@/components/modal'; -import { Form } from './form'; -import { Todos } from './todos'; - -import styles from './todo.module.css'; - -interface TodoProps { - onClose: () => void; - show: boolean; -} - -export function Todo({ onClose, show }: TodoProps) { - return ( - - Todos - - - - ); -} diff --git a/src/components/toolbox/todo/todos/index.ts b/src/components/toolbox/todo/todos/index.ts deleted file mode 100644 index 55b9f3d..0000000 --- a/src/components/toolbox/todo/todos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Todos } from './todos'; diff --git a/src/components/toolbox/todo/todos/todo/index.ts b/src/components/toolbox/todo/todos/todo/index.ts deleted file mode 100644 index b54a84e..0000000 --- a/src/components/toolbox/todo/todos/todo/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Todo } from './todo'; diff --git a/src/components/toolbox/todo/todos/todo/todo.module.css b/src/components/toolbox/todo/todos/todo/todo.module.css deleted file mode 100644 index b239bcd..0000000 --- a/src/components/toolbox/todo/todos/todo/todo.module.css +++ /dev/null @@ -1,45 +0,0 @@ -.wrapper { - display: flex; - column-gap: 4px; - align-items: center; - height: 45px; - padding: 4px; - margin-top: 12px; - background-color: var(--color-neutral-50); - border: 1px solid var(--color-neutral-200); - border-radius: 8px; - - & .checkbox { - display: block; - margin: 0 8px 0 4px; - } - - & .textbox { - flex-grow: 1; - min-width: 0; - height: 100%; - font-size: var(--font-sm); - color: var(--color-foreground); - background-color: transparent; - border: none; - outline: none; - - &.done { - color: var(--color-foreground-subtle); - text-decoration: line-through; - } - } - - & button { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - aspect-ratio: 1 / 1; - color: #f43f5e; - cursor: pointer; - background-color: rgb(244 63 94 / 15%); - border: none; - border-radius: 4px; - } -} diff --git a/src/components/toolbox/todo/todos/todo/todo.tsx b/src/components/toolbox/todo/todos/todo/todo.tsx deleted file mode 100644 index 3861d7f..0000000 --- a/src/components/toolbox/todo/todos/todo/todo.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FaRegTrashAlt } from 'react-icons/fa/index'; - -import { useTodoStore } from '@/stores/todo'; -import { cn } from '@/helpers/styles'; - -import styles from './todo.module.css'; - -interface TodoProps { - done: boolean; - id: string; - todo: string; -} - -export function Todo({ done, id, todo }: TodoProps) { - const deleteTodo = useTodoStore(state => state.deleteTodo); - const toggleTodo = useTodoStore(state => state.toggleTodo); - const editTodo = useTodoStore(state => state.editTodo); - - const handleCheck = () => toggleTodo(id); - const handleDelete = () => deleteTodo(id); - - return ( - - - editTodo(id, e.target.value)} - /> - - - - - ); -} diff --git a/src/components/toolbox/todo/todos/todos.module.css b/src/components/toolbox/todo/todos/todos.module.css deleted file mode 100644 index fdbd99d..0000000 --- a/src/components/toolbox/todo/todos/todos.module.css +++ /dev/null @@ -1 +0,0 @@ -/* WIP */ diff --git a/src/components/toolbox/todo/todos/todos.tsx b/src/components/toolbox/todo/todos/todos.tsx deleted file mode 100644 index 0b81812..0000000 --- a/src/components/toolbox/todo/todos/todos.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Todo } from './todo'; - -import { useTodoStore } from '@/stores/todo'; - -import styles from './todos.module.css'; - -export function Todos() { - const todos = useTodoStore(state => state.todos); - - return ( - - {todos.map(todo => ( - - ))} - - ); -} diff --git a/src/stores/note/index.ts b/src/stores/note/index.ts deleted file mode 100644 index 9ec59e5..0000000 --- a/src/stores/note/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { create } from 'zustand'; -import { createJSONStorage, persist } from 'zustand/middleware'; -import merge from 'deepmerge'; - -import { type NoteState, createState } from './note.state'; -import { type NoteActions, createActions } from './note.actions'; - -export const useNoteStore = create()( - persist( - (...a) => ({ - ...createState(...a), - ...createActions(...a), - }), - { - merge: (persisted, current) => - merge( - current, - // @ts-ignore - persisted, - ), - name: 'moodist-note', - partialize: state => ({ note: state.note }), - skipHydration: true, - storage: createJSONStorage(() => localStorage), - version: 0, - }, - ), -); diff --git a/src/stores/note/note.actions.ts b/src/stores/note/note.actions.ts deleted file mode 100644 index d7bab4c..0000000 --- a/src/stores/note/note.actions.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { StateCreator } from 'zustand'; - -import type { NoteState } from './note.state'; - -export interface NoteActions { - clear: () => void; - restore: () => void; - write: (note: string) => void; -} - -export const createActions: StateCreator< - NoteActions & NoteState, - [], - [], - NoteActions -> = (set, get) => { - return { - clear() { - if (!get().note) return; - - set({ history: get().note, note: '' }); - }, - - restore() { - if (!get().history) return; - - set({ history: null, note: get().history! }); - }, - - write(note) { - set({ history: null, note }); - }, - }; -}; diff --git a/src/stores/note/note.state.ts b/src/stores/note/note.state.ts deleted file mode 100644 index 2e00df0..0000000 --- a/src/stores/note/note.state.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { StateCreator } from 'zustand'; - -import type { NoteActions } from './note.actions'; -import { count } from '@/helpers/counter'; - -export interface NoteState { - characters: () => number; - history: string | null; - note: string; - words: () => number; -} - -export const createState: StateCreator< - NoteState & NoteActions, - [], - [], - NoteState -> = (set, get) => ({ - characters() { - return count(get().note).characters; - }, - history: null, - note: '', - words() { - return count(get().note).words; - }, -}); diff --git a/src/stores/pomodoro/index.ts b/src/stores/pomodoro/index.ts deleted file mode 100644 index d717db5..0000000 --- a/src/stores/pomodoro/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from 'zustand'; - -interface PomodoroStore { - running: boolean; - setRunning: (value: boolean) => void; -} - -export const usePomodoroStore = create()(set => ({ - running: false, - setRunning(value: boolean) { - set({ running: value }); - }, -})); diff --git a/src/stores/todo.ts b/src/stores/todo.ts deleted file mode 100644 index eb2101e..0000000 --- a/src/stores/todo.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { create } from 'zustand'; -import { createJSONStorage, persist } from 'zustand/middleware'; -import merge from 'deepmerge'; -import { v4 as uuid } from 'uuid'; - -interface TodoStore { - addTodo: (todo: string) => void; - deleteTodo: (id: string) => void; - editTodo: (id: string, newTodo: string) => void; - todos: Array<{ - createdAt: number; - done: boolean; - id: string; - todo: string; - }>; - toggleTodo: (id: string) => void; -} - -export const useTodoStore = create()( - persist( - (set, get) => ({ - addTodo(todo) { - set({ - todos: [ - { - createdAt: Date.now(), - done: false, - id: uuid(), - todo, - }, - ...get().todos, - ], - }); - }, - - deleteTodo(id) { - set({ - todos: get().todos.filter(todo => todo.id !== id), - }); - }, - - editTodo(id, newTodo) { - set({ - todos: get().todos.map(todo => { - if (todo.id !== id) return todo; - - return { - ...todo, - todo: newTodo, - }; - }), - }); - }, - - todos: [], - - toggleTodo(id) { - set({ - todos: get().todos.map(todo => { - if (todo.id !== id) return todo; - - return { - ...todo, - done: !todo.done, - }; - }), - }); - }, - }), - { - merge: (persisted, current) => - merge(current, persisted as Partial), - - name: 'moodist-todos', - partialize: state => ({ todos: state.todos }), - skipHydration: true, - storage: createJSONStorage(() => localStorage), - version: 0, - }, - ), -);
{PHASE_LABELS[currentPhase]}
Super simple countdown timer.
- {formatTime(elapsedTime)}
- {characters} character{characters !== 1 && 's'} • {words} word - {words !== 1 && 's'} -
- {completions[selectedTab] || 0} completed -