mirror of
https://github.com/remvze/moodist.git
synced 2025-12-17 00:44:14 +00:00
feat: remove all tools
This commit is contained in:
parent
1768ba1548
commit
b32d8b2803
34 changed files with 0 additions and 1122 deletions
|
|
@ -18,10 +18,6 @@ const paragraphs = [
|
|||
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options – just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.',
|
||||
title: 'Create Your Soundscape',
|
||||
},
|
||||
{
|
||||
body: 'Moodist offers more than ambient sounds with its suite of productivity tools to keep you organized and focused. Use the built-in pomodoro timer for structured work intervals, jot down ideas in the notepad, and track tasks with the to-do list (coming soon). These tools integrate seamlessly with the ambient soundscapes, creating a personalized environment that fosters focus and relaxation.',
|
||||
title: 'A Productivity Toolbox',
|
||||
},
|
||||
{
|
||||
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!",
|
||||
title: 'Sounds for Every Moment',
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import { IoMdFlower } from 'react-icons/io/index';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
export function BreathingExercise() {
|
||||
return (
|
||||
<Item
|
||||
href="/tools/breathing-exercises"
|
||||
icon={<IoMdFlower />}
|
||||
label="Breathing Exercise"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { MdOutlineTimer } from 'react-icons/md/index';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
export function CountdownTimer() {
|
||||
return (
|
||||
<Item
|
||||
href="https://timesy.app"
|
||||
icon={<MdOutlineTimer />}
|
||||
label="Countdown Timer"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
export { Shuffle as ShuffleItem } from './shuffle';
|
||||
export { Share as ShareItem } from './share';
|
||||
export { Donate as DonateItem } from './donate';
|
||||
export { Notepad as NotepadItem } from './notepad';
|
||||
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 as SleepTimerItem } from './sleep-timer';
|
||||
export { CountdownTimer as CountdownTimerItem } from './countdown-timer';
|
||||
export { BreathingExercise as BreathingExerciseItem } from './breathing-exercise';
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import { MdNotes } from 'react-icons/md/index';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
import { useNoteStore } from '@/stores/note';
|
||||
|
||||
export function Notepad() {
|
||||
const note = useNoteStore(state => state.note);
|
||||
|
||||
return (
|
||||
<Item
|
||||
active={!!note.length}
|
||||
href="/tools/notepad"
|
||||
icon={<MdNotes />}
|
||||
label="Notepad"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { MdOutlineAvTimer } from 'react-icons/md/index';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
export function Pomodoro() {
|
||||
return (
|
||||
<Item href="/tools/pomodoro" icon={<MdOutlineAvTimer />} label="Pomodoro" />
|
||||
);
|
||||
}
|
||||
|
|
@ -8,14 +8,10 @@ import {
|
|||
ShuffleItem,
|
||||
ShareItem,
|
||||
DonateItem,
|
||||
NotepadItem,
|
||||
SourceItem,
|
||||
PomodoroItem,
|
||||
CountdownTimerItem,
|
||||
PresetsItem,
|
||||
ShortcutsItem,
|
||||
SleepTimerItem,
|
||||
BreathingExerciseItem,
|
||||
} from './items';
|
||||
import { Divider } from './divider';
|
||||
import { ShareLinkModal } from '@/components/modals/share-link';
|
||||
|
|
@ -104,12 +100,6 @@ export function Menu() {
|
|||
<ShuffleItem />
|
||||
<SleepTimerItem open={() => open('sleepTimer')} />
|
||||
|
||||
<Divider />
|
||||
<BreathingExerciseItem />
|
||||
<PomodoroItem />
|
||||
<NotepadItem />
|
||||
<CountdownTimerItem />
|
||||
|
||||
<Divider />
|
||||
<ShortcutsItem open={() => open('shortcuts')} />
|
||||
|
||||
|
|
|
|||
|
|
@ -21,18 +21,6 @@ export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
|
|||
keys: ['Shift', 'S'],
|
||||
label: 'Share Sounds',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'N'],
|
||||
label: 'Notepad',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'P'],
|
||||
label: 'Pomodoro Timer',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'B'],
|
||||
label: 'Breathing Exercise',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'T'],
|
||||
label: 'Sleep Timer',
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
---
|
||||
import { Container } from '../container';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const { text } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="about">
|
||||
<Container tight>
|
||||
<h2 class="title">About this tool</h2>
|
||||
<p class="text">{text}</p>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.about {
|
||||
margin-top: 75px;
|
||||
|
||||
& .title {
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& .text {
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,47 +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;
|
||||
|
||||
& .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%);
|
||||
}
|
||||
}
|
||||
|
||||
.selectBox {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 45px;
|
||||
padding: 0 12px;
|
||||
margin-top: 8px;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-neutral-100);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { Container } from '@/components/container';
|
||||
|
||||
import styles from './breathing.module.css';
|
||||
|
||||
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
|
||||
type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale';
|
||||
|
||||
export function Breathing() {
|
||||
const [selectedExercise, setSelectedExercise] =
|
||||
useState<Exercise>('4-7-8 Breathing');
|
||||
|
||||
const getAnimationPhases = (
|
||||
exercise: Exercise,
|
||||
): Array<'inhale' | 'holdInhale' | 'exhale' | 'holdExhale'> => {
|
||||
switch (exercise) {
|
||||
case 'Box Breathing':
|
||||
return ['inhale', 'holdInhale', 'exhale', 'holdExhale'];
|
||||
case 'Resonant Breathing':
|
||||
return ['inhale', 'exhale'];
|
||||
case '4-7-8 Breathing':
|
||||
return ['inhale', 'holdInhale', 'exhale'];
|
||||
default:
|
||||
return ['inhale', 'holdInhale', 'exhale', 'holdExhale'];
|
||||
}
|
||||
};
|
||||
|
||||
const getAnimationDurations = (exercise: Exercise) => {
|
||||
switch (exercise) {
|
||||
case 'Box Breathing':
|
||||
return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 };
|
||||
case 'Resonant Breathing':
|
||||
return { exhale: 5, inhale: 5 };
|
||||
case '4-7-8 Breathing':
|
||||
return { exhale: 8, holdInhale: 7, inhale: 4 };
|
||||
default:
|
||||
return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 };
|
||||
}
|
||||
};
|
||||
|
||||
const getLabel = (phase: Phase) => {
|
||||
switch (phase) {
|
||||
case 'inhale':
|
||||
return 'Inhale';
|
||||
case 'exhale':
|
||||
return 'Exhale';
|
||||
default:
|
||||
return 'Hold';
|
||||
}
|
||||
};
|
||||
|
||||
const [phase, setPhase] = useState<Phase>('inhale');
|
||||
const [durations, setDurations] = useState(
|
||||
getAnimationDurations(selectedExercise),
|
||||
);
|
||||
|
||||
const animationVariants = {
|
||||
exhale: {
|
||||
transform: 'translate(-50%, -50%) scale(1)',
|
||||
transition: { duration: durations.exhale },
|
||||
},
|
||||
holdExhale: {
|
||||
transform: 'translate(-50%, -50%) scale(1)',
|
||||
transition: { duration: durations.holdExhale || 4 },
|
||||
},
|
||||
holdInhale: {
|
||||
transform: 'translate(-50%, -50%) scale(1.5)',
|
||||
transition: { duration: durations.holdInhale || 4 },
|
||||
},
|
||||
inhale: {
|
||||
transform: 'translate(-50%, -50%) scale(1.5)',
|
||||
transition: { duration: durations.inhale },
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDurations(getAnimationDurations(selectedExercise));
|
||||
}, [selectedExercise]);
|
||||
|
||||
useEffect(() => {
|
||||
const phases = getAnimationPhases(selectedExercise);
|
||||
|
||||
let phaseIndex = 0;
|
||||
|
||||
setPhase(phases[phaseIndex]);
|
||||
|
||||
const interval = setInterval(
|
||||
() => {
|
||||
phaseIndex = (phaseIndex + 1) % phases.length;
|
||||
|
||||
setPhase(phases[phaseIndex]);
|
||||
},
|
||||
(durations[phases[phaseIndex]] || 4) * 1000,
|
||||
);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [selectedExercise, durations]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className={styles.exercise}>
|
||||
<motion.div
|
||||
animate={phase}
|
||||
className={styles.circle}
|
||||
key={selectedExercise}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<p className={styles.phase}>{getLabel(phase)}</p>
|
||||
</div>
|
||||
|
||||
<select
|
||||
className={styles.selectBox}
|
||||
value={selectedExercise}
|
||||
onChange={e => setSelectedExercise(e.target.value as Exercise)}
|
||||
>
|
||||
<option value="Box Breathing">Box Breathing</option>
|
||||
<option value="Resonant Breathing">Resonant Breathing</option>
|
||||
<option value="4-7-8 Breathing">4-7-8 Breathing</option>
|
||||
</select>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Breathing } from './breathing';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Tooltip content={tooltip} placement="bottom" showDelay={0}>
|
||||
<button
|
||||
className={cn(styles.button, smallIcon && styles.smallIcon)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Button } from './button';
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
---
|
||||
import { Container } from '../container';
|
||||
|
||||
interface Props {
|
||||
desc: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { desc, title } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<Container>
|
||||
<img alt="Moodist Logo" class="logo" src="/logo.svg" />
|
||||
<h1>{title}</h1>
|
||||
<p class="desc">{desc}</p>
|
||||
<p class="moodist">Part of <a href="/">Moodist</a></p>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding: 75px 0;
|
||||
text-align: center;
|
||||
|
||||
& .logo {
|
||||
display: block;
|
||||
width: 45px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 4px;
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.moodist {
|
||||
margin-top: 16px;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-foreground-subtle);
|
||||
|
||||
& a {
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Tooltip content={tooltip} placement="bottom" showDelay={0}>
|
||||
<button
|
||||
className={cn(
|
||||
styles.button,
|
||||
critical && styles.critical,
|
||||
recommended && styles.recommended,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Button } from './button';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Notepad } from './notepad';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
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 { 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';
|
||||
import { Container } from '@/components/container';
|
||||
|
||||
export function Notepad() {
|
||||
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();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.label}>Your Note</h2>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
icon={copying ? <FaCheck /> : <LuCopy />}
|
||||
tooltip="Copy Note"
|
||||
onClick={() => copy(note)}
|
||||
/>
|
||||
<Button
|
||||
icon={<LuDownload />}
|
||||
tooltip="Download Note"
|
||||
onClick={() => download('Moodit Note.txt', note)}
|
||||
/>
|
||||
<Button
|
||||
critical={!history}
|
||||
icon={history ? <FaUndo /> : <BiTrash />}
|
||||
recommended={!!history}
|
||||
tooltip={history ? 'Restore Note' : 'Clear Note'}
|
||||
onClick={() => (history ? restore() : clear())}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
dir="auto"
|
||||
placeholder="What is on your mind?"
|
||||
spellCheck={false}
|
||||
value={note}
|
||||
onChange={e => write(e.target.value)}
|
||||
/>
|
||||
|
||||
<p className={styles.counter}>
|
||||
{characters} character{characters !== 1 && 's'} • {words} word
|
||||
{words !== 1 && 's'}
|
||||
</p>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Pomodoro } from './pomodoro';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +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 { Button } from '../generics/button';
|
||||
import { Timer } from '@/components/timer';
|
||||
import { Container } from '@/components/container';
|
||||
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';
|
||||
|
||||
export function Pomodoro() {
|
||||
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<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const alarm = useSoundEffect('/sounds/alarm.mp3');
|
||||
|
||||
const defaultTimes = useMemo(
|
||||
() => ({
|
||||
long: 15 * 60,
|
||||
pomodoro: 25 * 60,
|
||||
short: 5 * 60,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [times, setTimes] = useLocalStorage<Record<string, number>>(
|
||||
'moodist-pomodoro-setting',
|
||||
defaultTimes,
|
||||
);
|
||||
|
||||
const [completions, setCompletions] = useState<Record<string, number>>({
|
||||
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 (
|
||||
<Container>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Pomodoro Timer</h2>
|
||||
|
||||
<div className={styles.button}>
|
||||
<Button
|
||||
icon={<IoMdSettings />}
|
||||
tooltip="Change Times"
|
||||
onClick={() => {
|
||||
setShowSetting(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Tabs selectedTab={selectedTab} tabs={tabs} onSelect={setSelectedTab} />
|
||||
<Timer timer={timer} />
|
||||
|
||||
<div className={styles.control}>
|
||||
<p className={styles.completed}>
|
||||
{completions[selectedTab] || 0} completed
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
icon={<FaUndo />}
|
||||
smallIcon
|
||||
tooltip="Restart"
|
||||
onClick={restart}
|
||||
/>
|
||||
<Button
|
||||
icon={running ? <FaPause /> : <FaPlay />}
|
||||
smallIcon
|
||||
tooltip={running ? 'Pause' : 'Start'}
|
||||
onClick={toggleRunning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Setting
|
||||
show={showSetting}
|
||||
times={times}
|
||||
onChange={times => {
|
||||
setShowSetting(false);
|
||||
setTimes(times);
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowSetting(false);
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Setting } from './setting';
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, number>) => void;
|
||||
onClose: () => void;
|
||||
show: boolean;
|
||||
times: Record<string, number>;
|
||||
}
|
||||
|
||||
export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
||||
const [values, setValues] = useState<Record<string, number | string>>(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<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const newValues: Record<string, number> = {};
|
||||
|
||||
Object.keys(values).forEach(name => {
|
||||
newValues[name] =
|
||||
typeof values[name] === 'number' ? values[name] : times[name];
|
||||
});
|
||||
|
||||
onChange(newValues);
|
||||
};
|
||||
|
||||
const handleCancel = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal lockBody={false} show={show} onClose={onClose}>
|
||||
<h2 className={styles.title}>Change Times</h2>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSubmit}>
|
||||
<Field
|
||||
id="pomodoro"
|
||||
label="Pomodoro"
|
||||
value={values.pomodoro}
|
||||
onChange={handleChange('pomodoro')}
|
||||
/>
|
||||
<Field
|
||||
id="short"
|
||||
label="Short Break"
|
||||
value={values.short}
|
||||
onChange={handleChange('short')}
|
||||
/>
|
||||
<Field
|
||||
id="long"
|
||||
label="Long Break"
|
||||
value={values.long}
|
||||
onChange={handleChange('long')}
|
||||
/>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<button type="button" onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className={styles.primary} type="submit">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface FieldProps {
|
||||
id: string;
|
||||
label: string;
|
||||
onChange: (value: number | string) => void;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
function Field({ id, label, onChange, value }: FieldProps) {
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={id}>
|
||||
{label} <span>(minutes)</span>
|
||||
</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
max={120}
|
||||
min={1}
|
||||
required
|
||||
type="number"
|
||||
value={typeof value === 'number' ? value / 60 : ''}
|
||||
onChange={e => {
|
||||
onChange(e.target.value === '' ? '' : Number(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Tabs } from './tabs';
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className={styles.tabs}>
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
className={cn(styles.tab, selectedTab === tab.id && styles.selected)}
|
||||
key={tab.id}
|
||||
onClick={() => onSelect(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/tools/hero.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import About from '@/components/tools/about.astro';
|
||||
import { Breathing } from '@/components/tools/breathing';
|
||||
---
|
||||
|
||||
<Layout title="Breathing Exercises Tool — Moodist">
|
||||
<Donate />
|
||||
<Hero desc="Simple breathing exercises." title="Breathing Exercises" />
|
||||
<Breathing client:load />
|
||||
<About
|
||||
text="Experience calm and focus with our simple breathing exercise tool. Designed to guide your breath, it helps reduce stress, enhance relaxation, and improve mindfulness in just a few minutes a day."
|
||||
/>
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/tools/hero.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import About from '@/components/tools/about.astro';
|
||||
import { Notepad as NotepadComponent } from '@/components/tools/notepad';
|
||||
---
|
||||
|
||||
<Layout title="Online Notepad — Moodist">
|
||||
<Donate />
|
||||
<Hero desc="Distraction-free online notepad." title="Notepad" />
|
||||
<NotepadComponent client:load />
|
||||
<About
|
||||
text="Capture your thoughts instantly with our simple online notepad. Lightweight and easy to use, it lets you jot down notes, ideas, and to-do lists anytime, anywhere, without distractions."
|
||||
/>
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/tools/hero.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import About from '@/components/tools/about.astro';
|
||||
import { Pomodoro as PomodoroTimer } from '@/components/tools/pomodoro';
|
||||
---
|
||||
|
||||
<Layout title="Pomodoro Timer — Moodist">
|
||||
<Donate />
|
||||
<Hero desc="Super simple pomodoro timer." title="Pomodoro Timer" />
|
||||
<PomodoroTimer client:load />
|
||||
<About
|
||||
text="Boost your productivity with our simple Pomodoro timer. Designed to help you stay focused, it breaks your work into manageable intervals with regular breaks, making it easier to stay on track and avoid burnout."
|
||||
/>
|
||||
<Footer />
|
||||
</Layout>
|
||||
Loading…
Add table
Reference in a new issue