feat: internationalize toolbar menu items and associated Modals

This commit is contained in:
yozuru 2025-04-20 04:30:45 +08:00
parent 10f59a55a6
commit e6dd34e31d
No known key found for this signature in database
36 changed files with 779 additions and 305 deletions

View file

@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { Slider } from '@/components/slider';
@ -14,15 +14,46 @@ interface Preset {
baseFrequency: number;
beatFrequency: number;
name: string;
translationKey: string;
}
const presets: Preset[] = [
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
{
baseFrequency: 100,
beatFrequency: 2,
name: 'Delta (Deep Sleep) 2 Hz',
translationKey: 'modals.generators.presets.delta',
},
{
baseFrequency: 100,
beatFrequency: 5,
name: 'Theta (Meditation) 5 Hz',
translationKey: 'modals.generators.presets.theta',
},
{
baseFrequency: 100,
beatFrequency: 10,
name: 'Alpha (Relaxation) 10 Hz',
translationKey: 'modals.generators.presets.alpha',
},
{
baseFrequency: 100,
beatFrequency: 20,
name: 'Beta (Focus) 20 Hz',
translationKey: 'modals.generators.presets.beta',
},
{
baseFrequency: 100,
beatFrequency: 40,
name: 'Gamma (Cognition) 40 Hz',
translationKey: 'modals.generators.presets.gamma',
},
{
baseFrequency: 440,
beatFrequency: 10,
name: 'Custom',
translationKey: 'modals.generators.presets.custom',
},
];
function computeBinauralBeatOscillatorFrequencies(
@ -36,6 +67,7 @@ function computeBinauralBeatOscillatorFrequencies(
}
export function BinauralModal({ onClose, show }: BinauralProps) {
const { t } = useTranslation();
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default to A4 note
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
@ -145,15 +177,14 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
}, [selectedPreset]);
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selected = e.target.value;
setSelectedPreset(selected);
const selectedName = e.target.value;
setSelectedPreset(selectedName);
if (selected === 'Custom') {
// Allow user to input custom frequencies
if (selectedName === 'Custom') {
return;
}
const preset = presets.find(p => p.name === selected);
const preset = presets.find(p => p.name === selectedName);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
@ -163,17 +194,17 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Binaural Beat</h2>
<p className={styles.desc}>Binaural beat generator.</p>
<h2 className={styles.title}>{t('modals.binaural.title')}</h2>
<p className={styles.desc}>{t('modals.binaural.description')}</p>
</header>
<div className={styles.fieldWrapper}>
<label>
Presets:
{t('modals.generators.presets-label')}
<select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => (
<option key={preset.name} value={preset.name}>
{preset.name}
{t(preset.translationKey)}
</option>
))}
</select>
@ -183,7 +214,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
<>
<div className={styles.fieldWrapper}>
<label>
Base Frequency (Hz):
{t('modals.generators.base-frequency-label')}
<input
max="1500"
min="20"
@ -198,7 +229,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
</div>
<div className={styles.fieldWrapper}>
<label>
Beat Frequency (Hz):
{t('modals.binaural.beat-frequency-label')}
<input
max="40"
min="0.1"
@ -213,9 +244,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
</div>
</>
)}
<div className={styles.fieldWrapper}>
<label>
Volume:
{t('modals.generators.volume-label')}
<Slider
className={styles.volume}
max={1}
@ -232,10 +264,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
disabled={isPlaying}
onClick={startSound}
>
Start
{t('common.start')}
</button>
<button disabled={!isPlaying} onClick={stopSound}>
Stop
{t('common.stop')}
</button>
</div>
</Modal>

View file

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { Exercise } from './exercise';
@ -9,9 +10,12 @@ interface TimerProps {
}
export function BreathingExerciseModal({ onClose, show }: TimerProps) {
const { t } = useTranslation(); // Get t function
return (
<Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Breathing Exercise</h2>
<h2 className={styles.title || 'modal-title'}>
{t('modals.breathing.title')}
</h2>
<Exercise />
</Modal>
);

View file

@ -1,8 +1,7 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next'; // Import
import { padNumber } from '@/helpers/number';
import styles from './exercise.module.css';
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
@ -17,21 +16,27 @@ const EXERCISE_PHASES: Record<Exercise, Phase[]> = {
const EXERCISE_DURATIONS: Record<Exercise, Partial<Record<Phase, number>>> = {
'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
'Resonant Breathing': { exhale: 5, inhale: 5 },
};
const PHASE_LABELS: Record<Phase, string> = {
exhale: 'Exhale',
holdExhale: 'Hold',
holdInhale: 'Hold',
inhale: 'Inhale',
const PHASE_LABEL_KEYS: Record<Phase, string> = {
exhale: 'modals.breathing.phases.exhale',
holdExhale: 'modals.breathing.phases.hold',
holdInhale: 'modals.breathing.phases.hold',
inhale: 'modals.breathing.phases.inhale',
};
const EXERCISE_SELECT_OPTIONS: { key: string; value: Exercise }[] = [
{ key: 'modals.breathing.exercises.478', value: '4-7-8 Breathing' },
{ key: 'modals.breathing.exercises.box', value: 'Box Breathing' },
{ key: 'modals.breathing.exercises.resonant', value: 'Resonant Breathing' },
];
export function Exercise() {
const { t } = useTranslation();
const [selectedExercise, setSelectedExercise] =
useState<Exercise>('4-7-8 Breathing');
const [phaseIndex, setPhaseIndex] = useState(0);
const phases = useMemo(
() => EXERCISE_PHASES[selectedExercise],
[selectedExercise],
@ -92,6 +97,8 @@ export function Exercise() {
return () => clearInterval(interval);
}, []);
const currentPhaseLabel = t(PHASE_LABEL_KEYS[currentPhase]);
return (
<>
<div className={styles.exercise}>
@ -105,7 +112,7 @@ export function Exercise() {
key={selectedExercise}
variants={animationVariants}
/>
<p className={styles.phase}>{PHASE_LABELS[currentPhase]}</p>
<p className={styles.phase}>{currentPhaseLabel}</p>
</div>
<div className={styles.selectWrapper}>
@ -114,9 +121,9 @@ export function Exercise() {
value={selectedExercise}
onChange={e => setSelectedExercise(e.target.value as Exercise)}
>
{Object.keys(EXERCISE_PHASES).map(exercise => (
<option key={exercise} value={exercise}>
{exercise}
{EXERCISE_SELECT_OPTIONS.map(option => (
<option key={option.value} value={option.value}>
{t(option.key)}
</option>
))}
</select>

View file

@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { Slider } from '@/components/slider';
@ -14,18 +14,50 @@ interface Preset {
baseFrequency: number;
beatFrequency: number;
name: string;
translationKey: string;
}
const presets: Preset[] = [
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
{
baseFrequency: 100,
beatFrequency: 2,
name: 'Delta (Deep Sleep) 2 Hz',
translationKey: 'modals.generators.presets.delta',
},
{
baseFrequency: 100,
beatFrequency: 5,
name: 'Theta (Meditation) 5 Hz',
translationKey: 'modals.generators.presets.theta',
},
{
baseFrequency: 100,
beatFrequency: 10,
name: 'Alpha (Relaxation) 10 Hz',
translationKey: 'modals.generators.presets.alpha',
},
{
baseFrequency: 100,
beatFrequency: 20,
name: 'Beta (Focus) 20 Hz',
translationKey: 'modals.generators.presets.beta',
},
{
baseFrequency: 100,
beatFrequency: 40,
name: 'Gamma (Cognition) 40 Hz',
translationKey: 'modals.generators.presets.gamma',
},
{
baseFrequency: 440,
beatFrequency: 10,
name: 'Custom',
translationKey: 'modals.generators.presets.custom',
},
];
export function IsochronicModal({ onClose, show }: IsochronicProps) {
const { t } = useTranslation();
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default A4 note
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
@ -164,17 +196,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Isochronic Tone</h2>
<p className={styles.desc}>Isochronic tone generator.</p>
<h2 className={styles.title}>{t('modals.isochronic.title')}</h2>
<p className={styles.desc}>{t('modals.isochronic.description')}</p>
</header>
<div className={styles.fieldWrapper}>
<label>
Presets:
{t('modals.generators.presets-label')} {/* Use common key */}
<select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => (
<option key={preset.name} value={preset.name}>
{preset.name}
{t(preset.translationKey)}
</option>
))}
</select>
@ -184,7 +216,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
<>
<div className={styles.fieldWrapper}>
<label>
Base Frequency (Hz):
{t('modals.generators.base-frequency-label')}{' '}
{/* Use common key */}
<input
max="2000"
min="20"
@ -199,7 +232,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
</div>
<div className={styles.fieldWrapper}>
<label>
Tone Frequency (Hz):
{t('modals.isochronic.tone-frequency-label')}{' '}
{/* Use isochronic specific key */}
<input
max="40"
min="0.1"
@ -230,7 +264,7 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
)}
<div className={styles.fieldWrapper}>
<label>
Volume:
{t('modals.generators.volume-label')} {/* Use common key */}
<Slider
className={styles.volume}
max={1}
@ -241,16 +275,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
/>
</label>
</div>
<div className={styles.buttons}>
<button
className={styles.primary}
disabled={isPlaying}
onClick={startSound}
>
Start
{t('common.start')}
</button>
<button disabled={!isPlaying} onClick={stopSound}>
Stop
{t('common.stop')}
</button>
</div>
</Modal>

View file

@ -1,15 +1,17 @@
import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index';
import { useTranslation } from 'react-i18next';
import styles from './list.module.css';
import { useSoundStore } from '@/stores/sound';
import { usePresetStore } from '@/stores/preset';
import { Tooltip } from '@/components/tooltip';
interface ListProps {
close: () => void;
}
export function List({ close }: ListProps) {
const { t } = useTranslation();
const presets = usePresetStore(state => state.presets);
const changeName = usePresetStore(state => state.changeName);
const deletePreset = usePresetStore(state => state.deletePreset);
@ -19,26 +21,48 @@ export function List({ close }: ListProps) {
return (
<div className={styles.list}>
<h3 className={styles.title}>
Your Presets {presets.length > 0 && `(${presets.length})`}
{t('modals.presets.your-presets-title')}{' '}
{presets.length > 0 && `(${presets.length})`}
</h3>
{!presets.length && (
<p className={styles.empty}>You don&apos;t have any presets yet.</p>
<p className={styles.empty}>{t('modals.presets.empty')}</p>
)}
{presets.map(preset => (
<div className={styles.preset} key={preset.id}>
<input
placeholder="Untitled"
placeholder={t('common.untitled')}
type="text"
value={preset.label}
onChange={e => changeName(preset.id, e.target.value)}
/>
<button onClick={() => deletePreset(preset.id)}>
<Tooltip
showDelay={0}
content={
t('modals.presets.delete-button-tooltip') || 'Delete preset'
}
>
<button
aria-label={
t('modals.presets.delete-button-aria-label') ||
`Delete preset ${preset.label || t('common.untitled')}`
}
onClick={() => deletePreset(preset.id)}
>
<FaRegTrashAlt />
</button>
</Tooltip>
<Tooltip
content={t('modals.presets.play-button-tooltip') || 'Play preset'}
showDelay={0}
>
<button
className={styles.primary}
aria-label={
t('modals.presets.play-button-aria-label') ||
`Play preset ${preset.label || t('common.untitled')}`
}
onClick={() => {
override(preset.sounds);
play();
@ -47,6 +71,7 @@ export function List({ close }: ListProps) {
>
<FaPlay />
</button>
</Tooltip>
</div>
))}
</div>

View file

@ -1,5 +1,5 @@
import { useState, type FormEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/helpers/styles';
import { useSoundStore } from '@/stores/sound';
import { usePresetStore } from '@/stores/preset';
@ -7,6 +7,7 @@ import { usePresetStore } from '@/stores/preset';
import styles from './new.module.css';
export function New() {
const { t } = useTranslation();
const [name, setName] = useState('');
const noSelected = useSoundStore(state => state.noSelected());
@ -33,7 +34,7 @@ export function New() {
return (
<div className={styles.new}>
<h3 className={styles.title}>New Preset</h3>
<h3 className={styles.title}>{t('modals.presets.new-preset-title')}</h3>
<form
className={cn(styles.form, noSelected && styles.disabled)}
@ -41,18 +42,18 @@ export function New() {
>
<input
disabled={noSelected}
placeholder="Preset's Name"
placeholder={t('modals.presets.placeholder')}
required
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
<button disabled={noSelected}>Save</button>
<button disabled={noSelected}>{t('common.save')}</button>
</form>
{noSelected && (
<p className={styles.noSelected}>
To make a preset, first select some sounds.
{t('modals.presets.no-selected-warning')}
</p>
)}
</div>

View file

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { New } from './new';
import { List } from './list';
@ -10,9 +11,11 @@ interface PresetsModalProps {
}
export function PresetsModal({ onClose, show }: PresetsModalProps) {
const { t } = useTranslation();
return (
<Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Presets</h2>
<h2 className={styles.title}>{t('modals.presets.title')}</h2>
<New />
<div className={styles.divider} />
<List close={onClose} />

View file

@ -1,12 +1,13 @@
import { useMemo, useEffect, useState } from 'react';
import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { useCopy } from '@/hooks/use-copy';
import { useSoundStore } from '@/stores/sound';
import styles from './share-link.module.css';
import { Tooltip } from '@/components/tooltip'; // Import Tooltip
interface ShareLinkModalProps {
onClose: () => void;
@ -14,6 +15,7 @@ interface ShareLinkModalProps {
}
export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
const { t } = useTranslation();
const [isMounted, setIsMounted] = useState(false);
const sounds = useSoundStore(state => state.sounds);
const { copy, copying } = useCopy();
@ -51,16 +53,25 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
return (
<Modal show={show} onClose={onClose}>
<h1 className={styles.heading}>Share your sound selection!</h1>
<p className={styles.desc}>
Copy and send the following link to the person you want to share your
selection with.
</p>
<h1 className={styles.heading}>{t('modals.share-link.title')}</h1>
<p className={styles.desc}>{t('modals.share-link.description')}</p>
<div className={styles.inputWrapper}>
<input readOnly type="text" value={url} />
<button onClick={() => copy(url)}>
<Tooltip
content={copying ? t('common.copied') : t('common.copy')}
showDelay={0}
>
<button
aria-label={
copying
? t('common.copied')
: t('modals.share-link.copy-button-aria-label')
}
onClick={() => copy(url)}
>
{copying ? <IoCheckmark /> : <IoCopyOutline />}
</button>
</Tooltip>
</div>
</Modal>
);

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { useSoundStore } from '@/stores/sound';
@ -11,14 +11,16 @@ import { sounds } from '@/data/sounds';
import styles from './shared.module.css';
export function SharedModal() {
const { t } = useTranslation(); // Get t function
const override = useSoundStore(state => state.override);
const showSnackbar = useSnackbar();
const [isOpen, setIsOpen] = useState(false);
const [sharedSounds, setSharedSounds] = useState<
Array<{
id: string;
label: string;
labelKey: string;
volume: number;
}>
>([]);
@ -30,26 +32,26 @@ export function SharedModal() {
if (share) {
try {
const parsed = JSON.parse(decodeURIComponent(share));
const allSounds: Record<string, string> = {};
// Map sound IDs to their labelKeys for quick lookup
const allSoundLabelKeys: Record<string, string> = {};
sounds.categories.forEach(category => {
category.sounds.forEach(sound => {
allSounds[sound.id] = sound.label;
allSoundLabelKeys[sound.id] = sound.labelKey; // Get the labelKey
});
});
const _sharedSounds: Array<{
id: string;
label: string;
labelKey: string;
volume: number;
}> = [];
Object.keys(parsed).forEach(sound => {
if (allSounds[sound]) {
Object.keys(parsed).forEach(soundId => {
// Check if the soundId exists and has a labelKey
if (allSoundLabelKeys[soundId]) {
_sharedSounds.push({
id: sound,
label: allSounds[sound],
volume: Number(parsed[sound]),
id: soundId,
labelKey: allSoundLabelKeys[soundId], // Store the key
volume: Number(parsed[soundId]),
});
}
});
@ -59,12 +61,13 @@ export function SharedModal() {
setSharedSounds(_sharedSounds);
}
} catch (error) {
return;
console.error('Error parsing shared URL:', error); // Log error
return; // Stop execution if parsing fails
} finally {
history.pushState({}, '', location.href.split('?')[0]);
}
}
}, []);
}, []); // Run only once on mount
const handleOverride = () => {
const newSounds: Record<string, number> = {};
@ -75,34 +78,31 @@ export function SharedModal() {
override(newSounds);
setIsOpen(false);
showSnackbar('Done! You can now play the new selection.');
showSnackbar(t('modals.shared.snackbar-message'));
};
useCloseListener(() => setIsOpen(false));
return (
<Modal show={isOpen} onClose={() => setIsOpen(false)}>
<h1 className={styles.heading}>New sound mix detected!</h1>
<p className={styles.desc}>
Someone has shared the following mix with you. Would you want to
override your current selection?
</p>
<h1 className={styles.heading}>{t('modals.shared.title')}</h1>
<p className={styles.desc}>{t('modals.shared.description')}</p>
<div className={styles.sounds}>
{sharedSounds.map(sound => (
<div className={styles.sound} key={sound.id}>
{sound.label}
{t(sound.labelKey)}
</div>
))}
</div>
<div className={styles.footer}>
<button className={cn(styles.button)} onClick={() => setIsOpen(false)}>
Cancel
{t('common.cancel')}
</button>
<button
className={cn(styles.button, styles.primary)}
onClick={handleOverride}
>
Override
{t('common.override')}
</button>
</div>
</Modal>

View file

@ -1,69 +1,47 @@
import { Modal } from '@/components/modal';
import styles from './shortcuts.module.css';
// src/components/modals/shortcuts/shortcuts.tsx
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; // Assuming Modal component is stable
import styles from './shortcuts.module.css'; // Assuming styles are correct
interface ShortcutsModalProps {
onClose: () => void;
show: boolean;
onClose: () => void; // Function to close the modal
show: boolean; // Boolean to control modal visibility
}
export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
const shortcuts = [
{
keys: ['Shift', 'H'],
label: 'Shortcuts List',
},
{
keys: ['Shift', 'Alt', 'P'],
label: 'Presets',
},
{
keys: ['Shift', 'S'],
label: 'Share Sounds',
},
{
keys: ['Shift', 'Alt', 'T'],
label: 'Sleep Timer',
},
{
keys: ['Shift', 'C'],
label: 'Countdown Timer',
},
{
keys: ['Shift', 'P'],
label: 'Pomodoro',
},
{
keys: ['Shift', 'N'],
label: 'Notepad',
},
{
keys: ['Shift', 'T'],
label: 'Todo Checklist',
},
{
keys: ['Shift', 'B'],
label: 'Breathing Exercise',
},
{
keys: ['Shift', 'Space'],
label: 'Toggle Play',
},
{
keys: ['Shift', 'R'],
label: 'Unselect All Sounds',
},
interface ShortcutItem {
keys: string[];
labelKey: string;
}
const shortcutsList: ShortcutItem[] = [
{ keys: ['Shift', 'H'], labelKey: 'toolbar.items.shortcuts' }, // Reusing toolbar item key
{ keys: ['Shift', 'Alt', 'P'], labelKey: 'toolbar.items.presets' },
{ keys: ['Shift', 'S'], labelKey: 'toolbar.items.share' },
{ keys: ['Shift', 'Alt', 'T'], labelKey: 'toolbar.items.sleep-timer' },
{ keys: ['Shift', 'C'], labelKey: 'toolbar.items.countdown' },
{ keys: ['Shift', 'P'], labelKey: 'toolbar.items.pomodoro' },
{ keys: ['Shift', 'N'], labelKey: 'toolbar.items.notepad' },
{ keys: ['Shift', 'T'], labelKey: 'toolbar.items.todo' },
{ keys: ['Shift', 'B'], labelKey: 'toolbar.items.breathing' },
{ keys: ['Shift', 'Space'], labelKey: 'modals.shortcuts.labels.toggle-play' }, // Specific key
{ keys: ['Shift', 'R'], labelKey: 'modals.shortcuts.labels.unselect-all' }, // Specific key
];
export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
const { t } = useTranslation();
return (
<Modal show={show} onClose={onClose}>
<h1 className={styles.heading}>Keyboard Shortcuts</h1>
<h1 className={styles.heading}>{t('modals.shortcuts.title')}</h1>
<div className={styles.shortcuts}>
{shortcuts.map(shortcut => (
{shortcutsList.map(shortcut => (
// Render a Row for each shortcut item
// Use the labelKey as the React key for stability if IDs aren't available
<Row
key={shortcut.label}
key={shortcut.labelKey}
keys={shortcut.keys}
label={shortcut.label}
// Get the translated label using the defined labelKey
label={t(shortcut.labelKey)}
/>
))}
</div>
@ -90,10 +68,13 @@ function Row({ keys, label }: RowProps) {
);
}
// Props for the Key component
interface KeyProps {
children: React.ReactNode;
children: React.ReactNode; // The text content (key name)
}
// Component to render a single keyboard key representation
function Key({ children }: KeyProps) {
// Simple div with styling for a key
return <div className={styles.key}>{children}</div>;
}

View file

@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { Timer } from './timer';
import { dispatch } from '@/lib/event';
@ -15,7 +15,37 @@ interface SleepTimerModalProps {
show: boolean;
}
interface FieldProps {
labelKey: string;
onChange: (value: string) => void;
value: string;
}
function Field({ labelKey, onChange, value }: FieldProps) {
const { t } = useTranslation();
const label = t(labelKey);
return (
<div className={styles.field}>
<label className={styles.label} htmlFor={labelKey}>
{' '}
{label}
</label>
<input
className={styles.input}
id={labelKey}
max="59"
min="0"
required
type="number"
value={value}
onChange={e => onChange(e.target.value === '' ? '' : e.target.value)}
/>
</div>
);
}
export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
const { t } = useTranslation();
const setActive = useSleepTimerStore(state => state.set);
const noSelected = useSoundStore(state => state.noSelected());
@ -91,21 +121,27 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Sleep Timer</h2>
<p className={styles.desc}>
Stop sounds after a certain amount of time.
</p>
<h2 className={styles.title}>{t('modals.sleep-timer.title')}</h2>
<p className={styles.desc}>{t('modals.sleep-timer.description')}</p>
</header>
<form onSubmit={handleSubmit}>
<div className={styles.controls}>
<div className={styles.inputs}>
{!running && (
<Field label="Hours" value={hours} onChange={setHours} />
<Field
labelKey="modals.sleep-timer.hours-label"
value={hours}
onChange={setHours}
/>
)}
{!running && (
<Field label="Minutes" value={minutes} onChange={setMinutes} />
<Field
labelKey="modals.sleep-timer.minutes-label"
value={minutes}
onChange={setMinutes}
/>
)}
</div>
@ -118,7 +154,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
type="button"
onClick={handleReset}
>
Reset
{t('common.reset')}
</button>
)}
@ -127,7 +163,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
className={cn(styles.button, styles.primary)}
type="submit"
>
Start
{t('common.start')}
</button>
)}
</div>
@ -136,29 +172,3 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
</Modal>
);
}
interface FieldProps {
label: string;
onChange: (value: string) => void;
value: string;
}
function Field({ label, onChange, value }: FieldProps) {
return (
<div className={styles.field}>
<label className={styles.label} htmlFor={label.toLocaleLowerCase()}>
{label}
</label>
<input
className={styles.input}
id={label.toLocaleLowerCase()}
max="59"
min="0"
required
type="number"
value={value}
onChange={e => onChange(e.target.value === '' ? '' : e.target.value)}
/>
</div>
);
}

View file

@ -1,5 +1,5 @@
import { FaHeadphonesAlt } from 'react-icons/fa/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
interface BinauralProps {
@ -7,7 +7,13 @@ interface BinauralProps {
}
export function Binaural({ open }: BinauralProps) {
const { t } = useTranslation();
return (
<Item icon={<FaHeadphonesAlt />} label="Binaural Beats" onClick={open} />
<Item
icon={<FaHeadphonesAlt />}
label={t('toolbar.items.binaural')}
onClick={open}
/>
);
}

View file

@ -1,5 +1,5 @@
import { IoMdFlower } from 'react-icons/io/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
interface BreathingExerciseProps {
@ -7,10 +7,12 @@ interface BreathingExerciseProps {
}
export function BreathingExercise({ open }: BreathingExerciseProps) {
const { t } = useTranslation();
return (
<Item
icon={<IoMdFlower />}
label="Breathing Exercise"
label={t('toolbar.items.breathing')}
shortcut="Shift + B"
onClick={open}
/>

View file

@ -1,5 +1,5 @@
import { MdOutlineTimer } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
interface CountdownProps {
@ -7,10 +7,12 @@ interface CountdownProps {
}
export function Countdown({ open }: CountdownProps) {
const { t } = useTranslation();
return (
<Item
icon={<MdOutlineTimer />}
label="Countdown Timer"
label={t('toolbar.items.countdown')}
shortcut="Shift + C"
onClick={open}
/>

View file

@ -1,13 +1,15 @@
import { SiBuymeacoffee } from 'react-icons/si/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
export function Donate() {
const { t } = useTranslation();
return (
<Item
href="https://buymeacoffee.com/remvze"
icon={<SiBuymeacoffee />}
label="Buy Me a Coffee"
icon={<SiBuymeacoffee />} // Icon
label={t('toolbar.items.buy-me-a-coffee')}
/>
);
}

View file

@ -1,5 +1,5 @@
import { TbWaveSine } from 'react-icons/tb/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
interface IsochronicProps {
@ -7,5 +7,13 @@ interface IsochronicProps {
}
export function Isochronic({ open }: IsochronicProps) {
return <Item icon={<TbWaveSine />} label="Isochronic Tones" onClick={open} />;
const { t } = useTranslation(); // Get t function
return (
<Item
icon={<TbWaveSine />}
label={t('toolbar.items.isochronic')}
onClick={open}
/>
);
}

View file

@ -1,5 +1,5 @@
import { MdNotes } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
import { useNoteStore } from '@/stores/note';
@ -9,13 +9,14 @@ interface NotepadProps {
}
export function Notepad({ open }: NotepadProps) {
const { t } = useTranslation();
const note = useNoteStore(state => state.note);
return (
<Item
active={!!note.length}
icon={<MdNotes />}
label="Notepad"
label={t('toolbar.items.notepad')}
shortcut="Shift + N"
onClick={open}
/>

View file

@ -1,7 +1,6 @@
import { MdOutlineAvTimer } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
import { usePomodoroStore } from '@/stores/pomodoro';
interface PomodoroProps {
@ -9,13 +8,14 @@ interface PomodoroProps {
}
export function Pomodoro({ open }: PomodoroProps) {
const { t } = useTranslation();
const running = usePomodoroStore(state => state.running);
return (
<Item
active={running}
icon={<MdOutlineAvTimer />}
label="Pomodoro"
label={t('toolbar.items.pomodoro')}
shortcut="Shift + P"
onClick={open}
/>

View file

@ -1,5 +1,5 @@
import { RiPlayListFill } from 'react-icons/ri/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
interface PresetsProps {
@ -7,10 +7,11 @@ interface PresetsProps {
}
export function Presets({ open }: PresetsProps) {
const { t } = useTranslation();
return (
<Item
icon={<RiPlayListFill />}
label="Your Presets"
label={t('toolbar.items.presets')}
shortcut="Shift + Alt + P"
onClick={open}
/>

View file

@ -1,5 +1,5 @@
import { IoShareSocialSharp } from 'react-icons/io5/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
import { useSoundStore } from '@/stores/sound';
@ -9,13 +9,14 @@ interface ShareProps {
}
export function Share({ open }: ShareProps) {
const { t } = useTranslation();
const noSelected = useSoundStore(state => state.noSelected());
return (
<Item
disabled={noSelected}
icon={<IoShareSocialSharp />}
label="Share Sounds"
label={t('toolbar.items.share')}
shortcut="Shift + S"
onClick={open}
/>

View file

@ -1,5 +1,5 @@
import { MdKeyboardCommandKey } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
interface ShortcutsProps {
@ -7,10 +7,12 @@ interface ShortcutsProps {
}
export function Shortcuts({ open }: ShortcutsProps) {
const { t } = useTranslation();
return (
<Item
icon={<MdKeyboardCommandKey />}
label="Shortcuts"
label={t('toolbar.items.shortcuts')}
shortcut="Shift + H"
onClick={open}
/>

View file

@ -1,10 +1,10 @@
import { BiShuffle } from 'react-icons/bi/index';
import { useTranslation } from 'react-i18next';
import { useSoundStore } from '@/stores/sound';
import { Item } from '../item';
export function Shuffle() {
const { t } = useTranslation();
const shuffle = useSoundStore(state => state.shuffle);
const locked = useSoundStore(state => state.locked);
@ -12,7 +12,7 @@ export function Shuffle() {
<Item
disabled={locked}
icon={<BiShuffle />}
label="Shuffle Sounds"
label={t('toolbar.items.shuffle')}
onClick={shuffle}
/>
);

View file

@ -1,5 +1,5 @@
import { IoMoonSharp } from 'react-icons/io5/index';
import { useTranslation } from 'react-i18next';
import { useSleepTimerStore } from '@/stores/sleep-timer';
import { Item } from '../item';
@ -8,13 +8,14 @@ interface SleepTimerProps {
}
export function SleepTimer({ open }: SleepTimerProps) {
const { t } = useTranslation();
const active = useSleepTimerStore(state => state.active);
return (
<Item
active={active}
icon={<IoMoonSharp />}
label="Sleep Timer"
label={t('toolbar.items.sleep-timer')}
shortcut="Shift + Alt + T"
onClick={open}
/>

View file

@ -1,13 +1,15 @@
import { LuGithub } from 'react-icons/lu/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
export function Source() {
const { t } = useTranslation();
return (
<Item
href="https://github.com/remvze/moodist"
icon={<LuGithub />}
label="Source Code"
label={t('toolbar.items.source-code')}
/>
);
}

View file

@ -1,5 +1,5 @@
import { MdTaskAlt } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item';
interface TodoProps {
@ -7,10 +7,12 @@ interface TodoProps {
}
export function Todo({ open }: TodoProps) {
const { t } = useTranslation();
return (
<Item
icon={<MdTaskAlt />}
label="Todo Checklist"
label={t('toolbar.items.todo')}
shortcut="Shift + T"
onClick={open}
/>

View file

@ -3,7 +3,7 @@ import { IoMenu, IoClose } from 'react-icons/io5/index';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useHotkeys } from 'react-hotkeys-hook';
import { AnimatePresence, motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import {
ShuffleItem,
ShareItem,
@ -39,6 +39,7 @@ import { useCloseListener } from '@/hooks/use-close-listener';
import { closeModals } from '@/lib/modal';
export function Menu() {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const noSelected = useSoundStore(state => state.noSelected());
@ -100,7 +101,10 @@ export function Menu() {
<div className={styles.wrapper}>
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
<DropdownMenu.Trigger asChild>
<button aria-label="Menu" className={styles.menuButton}>
<button
aria-label={t('toolbar.menu-aria-label')}
className={styles.menuButton}
>
{isOpen ? <IoClose /> : <IoMenu />}
</button>
</DropdownMenu.Trigger>
@ -143,7 +147,9 @@ export function Menu() {
<Divider />
<div className={styles.globalVolume}>
<label htmlFor="global-volume">Global Volume</label>
<label htmlFor="global-volume">
{t('toolbar.global-volume-label')}
</label>
<Slider
max={100}
min={0}

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { useSoundEffect } from '@/hooks/use-sound-effect';
@ -14,6 +14,7 @@ interface CountdownProps {
}
export function Countdown({ onClose, show }: CountdownProps) {
const { t } = useTranslation();
const [hours, setHours] = useState(0);
const [minutes, setMinutes] = useState(0);
const [seconds, setSeconds] = useState(0);
@ -73,8 +74,8 @@ export function Countdown({ onClose, show }: CountdownProps) {
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Countdown Timer</h2>
<p className={styles.desc}>Super simple countdown timer.</p>
<h2 className={styles.title}>{t('modals.countdown.title')}</h2>
<p className={styles.desc}>{t('modals.countdown.description')}</p>
</header>
{isFormVisible ? (
@ -82,21 +83,11 @@ export function Countdown({ onClose, show }: CountdownProps) {
<div className={styles.inputContainer}>
<input
className={styles.input}
placeholder="HH"
placeholder={t('modals.countdown.placeholder-hh') || 'HH'} // Placeholder
type="number"
value={hours}
onChange={e => setHours(Math.max(0, parseInt(e.target.value)))}
/>
<span>:</span>
<input
className={styles.input}
placeholder="MM"
type="number"
value={minutes}
value={hours === 0 ? '' : hours} // Show empty if 0
onChange={e =>
setMinutes(Math.max(0, Math.min(59, parseInt(e.target.value))))
setHours(Math.max(0, parseInt(e.target.value || '0')))
}
/>
@ -104,21 +95,34 @@ export function Countdown({ onClose, show }: CountdownProps) {
<input
className={styles.input}
placeholder="SS"
placeholder={t('modals.countdown.placeholder-mm') || 'MM'}
type="number"
value={seconds}
value={minutes === 0 ? '' : minutes} // Show empty if 0
onChange={e =>
setSeconds(Math.max(0, Math.min(59, parseInt(e.target.value))))
setMinutes(
Math.max(0, Math.min(59, parseInt(e.target.value || '0'))),
)
}
/>
<span>:</span>
<input
className={styles.input}
placeholder={t('modals.countdown.placeholder-ss') || 'SS'}
type="number"
value={seconds === 0 ? '' : seconds} // Show empty if 0
onChange={e =>
setSeconds(
Math.max(0, Math.min(59, parseInt(e.target.value || '0'))),
)
}
/>
</div>
<div className={styles.buttonContainer}>
<button
className={cn(styles.button, styles.primary)}
onClick={handleStart}
>
Start
{t('common.start')}
</button>
</div>
</div>
@ -128,17 +132,15 @@ export function Countdown({ onClose, show }: CountdownProps) {
<p className={styles.reverse}>- {formatTime(elapsedTime)}</p>
<span>{formatTime(timeLeft)}</span>
</div>
<div className={styles.buttonContainer}>
<button className={styles.button} onClick={handleBack}>
Back
{t('common.back')}
</button>
<button
className={cn(styles.button, styles.primary)}
onClick={toggleTimer}
>
{isActive ? 'Pause' : 'Start'}
{isActive ? t('common.pause') : t('common.start')}
</button>
</div>
</div>

View file

@ -3,7 +3,7 @@ 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 { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { Button } from './button';
@ -19,6 +19,7 @@ interface NotepadProps {
}
export function Notepad({ onClose, show }: NotepadProps) {
const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const note = useNoteStore(state => state.note);
@ -45,26 +46,42 @@ export function Notepad({ onClose, show }: NotepadProps) {
if (e.key === 'Escape') onClose();
};
const counterOptions = {
chars: characters,
chars_plural:
characters !== 1 ? t('common.plural-suffix', { defaultValue: 's' }) : '',
words: words,
words_plural:
words !== 1 ? t('common.plural-suffix', { defaultValue: 's' }) : '',
};
const clearOrRestoreTooltip = history
? t('modals.notepad.restore-tooltip')
: t('modals.notepad.clear-tooltip');
const copyTooltip = copying
? t('common.copied')
: t('modals.notepad.copy-tooltip');
return (
<Modal show={show} wide onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.label}>Your Note</h2>
<h2 className={styles.label}>{t('modals.notepad.title-label')}</h2>
<div className={styles.buttons}>
<Button
icon={copying ? <FaCheck /> : <LuCopy />}
tooltip="Copy Note"
tooltip={copyTooltip}
onClick={() => copy(note)}
/>
<Button
icon={<LuDownload />}
tooltip="Download Note"
onClick={() => download('Moodit Note.txt', note)}
tooltip={t('modals.notepad.download-tooltip')}
onClick={() => download('Moodist Note.txt', note)}
/>
<Button
critical={!history}
icon={history ? <FaUndo /> : <BiTrash />}
recommended={!!history}
tooltip={history ? 'Restore Note' : 'Clear Note'}
tooltip={clearOrRestoreTooltip}
onClick={() => (history ? restore() : clear())}
/>
</div>
@ -73,7 +90,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
<textarea
className={styles.textarea}
dir="auto"
placeholder="What is on your mind?"
placeholder={t('modals.notepad.placeholder')}
ref={textareaRef}
spellCheck={false}
value={note}
@ -82,8 +99,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
/>
<p className={styles.counter}>
{characters} character{characters !== 1 && 's'} {words} word
{words !== 1 && 's'}
{t('modals.notepad.counter-stats', counterOptions)}
</p>
</Modal>
);

View file

@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index';
import { IoMdSettings } from 'react-icons/io/index';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { Button } from '../generics/button';
import { Timer } from './timer';
@ -12,7 +12,6 @@ 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 {
@ -22,6 +21,7 @@ interface PomodoroProps {
}
export function Pomodoro({ onClose, open, show }: PomodoroProps) {
const { t } = useTranslation();
const [showSetting, setShowSetting] = useState(false);
const [selectedTab, setSelectedTab] = useState('pomodoro');
@ -56,11 +56,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
const tabs = useMemo(
() => [
{ id: 'pomodoro', label: 'Pomodoro' },
{ id: 'short', label: 'Break' },
{ id: 'long', label: 'Long Break' },
{ id: 'pomodoro', label: t('modals.pomodoro.tabs.pomodoro') },
{ id: 'short', label: t('modals.pomodoro.tabs.short-break') },
{ id: 'long', label: t('modals.pomodoro.tabs.long-break') },
],
[],
[t],
);
useCloseListener(() => setShowSetting(false));
@ -123,12 +123,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
<>
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Pomodoro Timer</h2>
<h2 className={styles.title}>{t('modals.pomodoro.title')}</h2>
<div className={styles.button}>
<Button
icon={<IoMdSettings />}
tooltip="Change Times"
tooltip={t('modals.pomodoro.settings-tooltip')}
onClick={() => {
onClose();
setShowSetting(true);
@ -142,19 +141,21 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
<div className={styles.control}>
<p className={styles.completed}>
{completions[selectedTab] || 0} completed
{t('modals.pomodoro.completed', {
count: completions[selectedTab] || 0,
})}
</p>
<div className={styles.buttons}>
<Button
icon={<FaUndo />}
smallIcon
tooltip="Restart"
tooltip={t('common.restart')}
onClick={restart}
/>
<Button
icon={running ? <FaPause /> : <FaPlay />}
smallIcon
tooltip={running ? 'Pause' : 'Start'}
tooltip={running ? t('common.pause') : t('common.start')}
onClick={toggleRunning}
/>
</div>

View file

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import styles from './setting.module.css';
@ -12,6 +12,7 @@ interface SettingProps {
}
export function Setting({ onChange, onClose, show, times }: SettingProps) {
const { t } = useTranslation();
const [values, setValues] = useState<Record<string, number | string>>(times);
useEffect(() => {
@ -46,34 +47,34 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
return (
<Modal lockBody={false} show={show} onClose={onClose}>
<h2 className={styles.title}>Change Times</h2>
<h2 className={styles.title}>{t('modals.pomodoro.settings.title')}</h2>
<form className={styles.form} onSubmit={handleSubmit}>
<Field
id="pomodoro"
label="Pomodoro"
labelKey="modals.pomodoro.settings.pomodoro-label"
value={values.pomodoro}
onChange={handleChange('pomodoro')}
/>
<Field
id="short"
label="Short Break"
labelKey="modals.pomodoro.settings.short-break-label"
value={values.short}
onChange={handleChange('short')}
/>
<Field
id="long"
label="Long Break"
labelKey="modals.pomodoro.settings.long-break-label"
value={values.long}
onChange={handleChange('long')}
/>
<div className={styles.buttons}>
<button type="button" onClick={handleCancel}>
Cancel
{t('common.cancel')}
</button>
<button className={styles.primary} type="submit">
Save
{t('common.save')}
</button>
</div>
</form>
@ -83,19 +84,22 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
interface FieldProps {
id: string;
label: string;
labelKey: string;
onChange: (value: number | string) => void;
value: number | string;
}
function Field({ id, label, onChange, value }: FieldProps) {
function Field({ id, labelKey, onChange, value }: FieldProps) {
const { t } = useTranslation(); // 获取翻译函数
return (
<div className={styles.field}>
<label className={styles.label} htmlFor={id}>
{label} <span>(minutes)</span>
{t(labelKey)}{' '}
<span>({t('modals.pomodoro.settings.minutes-unit')})</span>{' '}
</label>
<input
className={styles.input}
id={id}
max={120}
min={1}
required

View file

@ -1,10 +1,11 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useTodoStore } from '@/stores/todo';
import styles from './form.module.css';
export function Form() {
const { t } = useTranslation();
const [value, setValue] = useState('');
const addTodo = useTodoStore(state => state.addTodo);
@ -22,12 +23,12 @@ export function Form() {
<form onSubmit={handleSubmit}>
<div className={styles.wrapper}>
<input
placeholder="I have to ..."
placeholder={t('modals.todo.add-placeholder')}
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button type="submit">Add</button>
<button type="submit">{t('modals.todo.add-button')}</button>
</div>
</form>
);

View file

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal';
import { Form } from './form';
import { Todos } from './todos';
@ -10,11 +11,13 @@ interface TodoProps {
}
export function Todo({ onClose, show }: TodoProps) {
const { t } = useTranslation();
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Todo Checklist</h2>
<p className={styles.desc}>Super simple todo list.</p>
<h2 className={styles.title}>{t('modals.todo.title')}</h2>
<p className={styles.desc}>{t('modals.todo.description')}</p>
</header>
<Form />

View file

@ -1,11 +1,12 @@
import { FaRegTrashAlt } from 'react-icons/fa/index';
import { useTranslation } from 'react-i18next';
import { Checkbox } from '@/components/checkbox';
import { useTodoStore } from '@/stores/todo';
import { cn } from '@/helpers/styles';
import styles from './todo.module.css';
import { Tooltip } from '@/components/tooltip';
interface TodoProps {
done: boolean;
@ -14,6 +15,7 @@ interface TodoProps {
}
export function Todo({ done, id, todo }: TodoProps) {
const { t } = useTranslation();
const deleteTodo = useTodoStore(state => state.deleteTodo);
const toggleTodo = useTodoStore(state => state.toggleTodo);
const editTodo = useTodoStore(state => state.editTodo);
@ -32,9 +34,17 @@ export function Todo({ done, id, todo }: TodoProps) {
value={todo}
onChange={e => editTodo(id, e.target.value)}
/>
<button className={styles.delete} onClick={handleDelete}>
<Tooltip content={t('common.delete')} showDelay={0}>
<button
className={styles.delete}
aria-label={
t('modals.todo.delete-button-aria-label') || `Delete todo: ${todo}`
}
onClick={handleDelete}
>
<FaRegTrashAlt />
</button>
</Tooltip>
</div>
);
}

View file

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Todo } from './todo';
import { useTodoStore } from '@/stores/todo';
@ -5,13 +6,14 @@ import { useTodoStore } from '@/stores/todo';
import styles from './todos.module.css';
export function Todos() {
const { t } = useTranslation();
const todos = useTodoStore(state => state.todos);
const doneCount = useTodoStore(state => state.doneCount());
return (
<div className={styles.todos}>
<header>
<p className={styles.label}>Your Todos</p>
<p className={styles.label}>{t('modals.todo.your-todos-label')}</p>
<div className={styles.divider} />
<p className={styles.counter}>
{doneCount} / {todos.length}
@ -30,7 +32,7 @@ export function Todos() {
))}
</>
) : (
<p className={styles.empty}>You don&apos;t have any todos.</p>
<p className={styles.empty}>{t('modals.todo.empty')}</p>
)}
</div>
);

View file

@ -8,11 +8,156 @@
"en": "English",
"zh": "简体中文"
},
"common": {
"save": "Save",
"untitled": "Untitled",
"copy": "Copy",
"copied": "Copied!",
"override": "Override",
"done": "Done!",
"reset": "Reset",
"start": "Start",
"pause": "Pause",
"stop": "Stop",
"back": "Back",
"restart": "Restart",
"cancel": "Cancel",
"delete": "Delete",
"plural-suffix": "s"
},
"toolbar": {
"menu-aria-label": "Menu",
"global-volume-label": "Global Volume",
"items": {
"presets": "Your Presets",
"share": "Share Sounds",
"shuffle": "Shuffle Sounds",
"sleep-timer": "Sleep Timer",
"countdown": "Countdown Timer",
"pomodoro": "Pomodoro",
"notepad": "Notepad",
"todo": "Todo Checklist",
"breathing": "Breathing Exercise",
"binaural": "Binaural Beats",
"isochronic": "Isochronic Tones",
"shortcuts": "Shortcuts",
"buy-me-a-coffee": "Buy Me a Coffee",
"source-code": "Source Code"
}
},
"modals": {
"reload": {
"title": "New Content",
"description": "New content available, click on reload button to update.",
"reloadButton": "Reload"
"presets": {
"title": "Presets",
"your-presets-title": "Your Presets",
"empty": "You don't have any presets yet.",
"new-preset-title": "New Preset",
"placeholder": "Preset's Name",
"play-button-tooltip": "Play preset",
"play-button-aria-label": "Play preset {{label}}",
"delete-button-tooltip": "Delete preset",
"delete-button-aria-label": "Delete preset {{label}}",
"no-selected-warning": "To make a preset, first select some sounds."
},
"share-link": {
"title": "Share your sound selection!",
"description": "Copy and send the following link to the person you want to share your selection with.",
"copy-button-aria-label": "Copy link"
},
"shared": {
"title": "New sound mix detected!",
"description": "Someone has shared the following mix with you. Would you want to override your current selection?",
"snackbar-message": "Done! You can now play the new selection."
},
"sleep-timer": {
"title": "Sleep Timer",
"description": "Stop sounds after a certain amount of time.",
"hours-label": "Hours",
"minutes-label": "Minutes"
},
"countdown": {
"title": "Countdown Timer",
"description": "Super simple countdown timer.",
"placeholder-hh": "HH",
"placeholder-mm": "MM",
"placeholder-ss": "SS"
},
"pomodoro": {
"title": "Pomodoro Timer",
"settings-tooltip": "Change Times",
"completed": "{{count}} completed",
"tabs": {
"pomodoro": "Pomodoro",
"short-break": "Break",
"long-break": "Long Break"
},
"settings": {
"title": "Change Times",
"pomodoro-label": "Pomodoro",
"short-break-label": "Short Break",
"long-break-label": "Long Break",
"minutes-unit": "minutes"
}
},
"notepad": {
"title-label": "Your Note",
"copy-tooltip": "Copy Note",
"download-tooltip": "Download Note",
"clear-tooltip": "Clear Note",
"restore-tooltip": "Restore Note",
"placeholder": "What is on your mind?",
"counter-stats": "{{chars}} character{{chars_plural}} • {{words}} word{{words_plural}}"
},
"todo": {
"title": "Todo Checklist",
"description": "Super simple todo list.",
"add-placeholder": "I have to ...",
"add-button": "Add",
"your-todos-label": "Your Todos",
"empty": "You don't have any todos.",
"delete-button-aria-label": "Delete todo"
},
"breathing": {
"title": "Breathing Exercise",
"phases": {
"inhale": "Inhale",
"exhale": "Exhale",
"hold": "Hold"
},
"exercises": {
"box": "Box Breathing",
"resonant": "Resonant Breathing",
"478": "4-7-8 Breathing"
}
},
"generators": {
"presets-label": "Presets:",
"base-frequency-label": "Base Frequency (Hz):",
"volume-label": "Volume:",
"presets": {
"delta": "Delta (Deep Sleep) 2 Hz",
"theta": "Theta (Meditation) 5 Hz",
"alpha": "Alpha (Relaxation) 10 Hz",
"beta": "Beta (Focus) 20 Hz",
"gamma": "Gamma (Cognition) 40 Hz",
"custom": "Custom"
}
},
"binaural": {
"title": "Binaural Beat",
"description": "Binaural beat generator.",
"beat-frequency-label": "Beat Frequency (Hz):"
},
"isochronic": {
"title": "Isochronic Tone",
"description": "Isochronic tone generator.",
"tone-frequency-label": "Tone Frequency (Hz):"
},
"shortcuts": {
"title": "Keyboard Shortcuts",
"labels": {
"toggle-play": "Toggle Play",
"unselect-all": "Unselect All Sounds"
}
}
},
"buttons": {

View file

@ -8,11 +8,156 @@
"en": "English",
"zh": "简体中文"
},
"common": {
"save": "保存",
"untitled": "未命名",
"copy": "复制",
"copied": "已复制!",
"override": "覆盖",
"done": "完成!",
"reset": "重置",
"start": "开始",
"pause": "暂停",
"stop": "停止",
"back": "返回",
"restart": "重新开始",
"cancel": "取消",
"delete": "删除",
"plural-suffix": ""
},
"toolbar": {
"menu-aria-label": "菜单",
"global-volume-label": "全局音量",
"items": {
"presets": "预设",
"share": "分享声音",
"shuffle": "随机播放",
"sleep-timer": "睡眠定时器",
"countdown": "倒计时器",
"pomodoro": "番茄钟",
"notepad": "记事本",
"todo": "待办清单",
"breathing": "呼吸练习",
"binaural": "双耳节拍",
"isochronic": "等时声频",
"shortcuts": "快捷键",
"buy-me-a-coffee": "请我喝咖啡",
"source-code": "源代码"
}
},
"modals": {
"reload": {
"title": "发现新内容",
"description": "检测到可用新内容,点击“重新加载”按钮进行更新。",
"reloadButton": "重新加载"
"presets": {
"title": "预设",
"your-presets-title": "您的预设",
"empty": "您还没有任何预设。",
"new-preset-title": "新建预设",
"placeholder": "预设名称",
"play-button-tooltip": "播放预设",
"play-button-aria-label": "播放预设 {{label}}",
"delete-button-tooltip": "删除预设",
"delete-button-aria-label": "删除预设 {{label}}",
"no-selected-warning": "要创建预设,请先选择一些声音。"
},
"share-link": {
"title": "分享您的声音组合!",
"description": "复制下面的链接并发送给您想与之分享的人。",
"copy-button-aria-label": "复制链接"
},
"shared": {
"title": "检测到新的声音组合!",
"description": "有人与您分享了以下声音组合。您想覆盖当前的选择吗?",
"snackbar-message": "完成!您现在可以播放新的选择了。"
},
"sleep-timer": {
"title": "睡眠定时器",
"description": "在指定时间后停止播放声音。",
"hours-label": "小时",
"minutes-label": "分钟"
},
"countdown": {
"title": "倒计时器",
"description": "超级简单的倒计时器。",
"placeholder-hh": "时",
"placeholder-mm": "分",
"placeholder-ss": "秒"
},
"pomodoro": {
"title": "番茄钟",
"settings-tooltip": "更改时间",
"completed": "已完成 {{count}} 次",
"tabs": {
"pomodoro": "番茄钟",
"short-break": "短休息",
"long-break": "长休息"
},
"settings": {
"title": "更改时间",
"pomodoro-label": "番茄钟",
"short-break-label": "短休息",
"long-break-label": "长休息",
"minutes-unit": "分钟"
}
},
"notepad": {
"title-label": "您的笔记",
"copy-tooltip": "复制笔记",
"download-tooltip": "下载笔记",
"clear-tooltip": "清空笔记",
"restore-tooltip": "恢复笔记",
"placeholder": "您在想什么?",
"counter-stats": "共 {{chars}} 个字符"
},
"todo": {
"title": "待办清单",
"description": "超级简单的待办事项列表。",
"add-placeholder": "我需要做...",
"add-button": "添加",
"your-todos-label": "您的待办事项",
"empty": "您还没有任何待办事项。",
"delete-button-aria-label": "删除待办事项"
},
"breathing": {
"title": "呼吸练习",
"phases": {
"inhale": "吸气",
"exhale": "呼气",
"hold": "屏息"
},
"exercises": {
"box": "方形呼吸",
"resonant": "共振呼吸",
"478": "4-7-8 呼吸"
}
},
"generators": {
"presets-label": "预设:",
"base-frequency-label": "基础频率 (Hz):",
"volume-label": "音量:",
"presets": {
"delta": "Delta (深度睡眠) 2 Hz",
"theta": "Theta (冥想) 5 Hz",
"alpha": "Alpha (放松) 10 Hz",
"beta": "Beta (专注) 20 Hz",
"gamma": "Gamma (认知) 40 Hz",
"custom": "自定义"
}
},
"binaural": {
"title": "双耳节拍",
"description": "双耳节拍生成器。",
"beat-frequency-label": "节拍频率 (Hz):"
},
"isochronic": {
"title": "等时声频",
"description": "等时声频生成器。",
"tone-frequency-label": "声频频率 (Hz):"
},
"shortcuts": {
"title": "键盘快捷键",
"labels": {
"toggle-play": "播放/暂停",
"unselect-all": "取消全选声音"
}
}
},
"buttons": {