mirror of
https://github.com/remvze/moodist.git
synced 2025-12-18 09:24:14 +00:00
feat: internationalize toolbar menu items and associated Modals
This commit is contained in:
parent
10f59a55a6
commit
e6dd34e31d
36 changed files with 779 additions and 305 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
import { Slider } from '@/components/slider';
|
import { Slider } from '@/components/slider';
|
||||||
|
|
||||||
|
|
@ -14,15 +14,46 @@ interface Preset {
|
||||||
baseFrequency: number;
|
baseFrequency: number;
|
||||||
beatFrequency: number;
|
beatFrequency: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
translationKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const presets: Preset[] = [
|
const presets: Preset[] = [
|
||||||
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
|
{
|
||||||
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
|
baseFrequency: 100,
|
||||||
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
|
beatFrequency: 2,
|
||||||
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
|
name: 'Delta (Deep Sleep) 2 Hz',
|
||||||
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
|
translationKey: 'modals.generators.presets.delta',
|
||||||
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
|
},
|
||||||
|
{
|
||||||
|
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(
|
function computeBinauralBeatOscillatorFrequencies(
|
||||||
|
|
@ -36,6 +67,7 @@ function computeBinauralBeatOscillatorFrequencies(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BinauralModal({ onClose, show }: BinauralProps) {
|
export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default to A4 note
|
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default to A4 note
|
||||||
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference
|
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference
|
||||||
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
||||||
|
|
@ -145,15 +177,14 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||||
}, [selectedPreset]);
|
}, [selectedPreset]);
|
||||||
|
|
||||||
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const selected = e.target.value;
|
const selectedName = e.target.value;
|
||||||
setSelectedPreset(selected);
|
setSelectedPreset(selectedName);
|
||||||
|
|
||||||
if (selected === 'Custom') {
|
if (selectedName === 'Custom') {
|
||||||
// Allow user to input custom frequencies
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preset = presets.find(p => p.name === selected);
|
const preset = presets.find(p => p.name === selectedName);
|
||||||
if (preset) {
|
if (preset) {
|
||||||
setBaseFrequency(preset.baseFrequency);
|
setBaseFrequency(preset.baseFrequency);
|
||||||
setBeatFrequency(preset.beatFrequency);
|
setBeatFrequency(preset.beatFrequency);
|
||||||
|
|
@ -163,17 +194,17 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<h2 className={styles.title}>Binaural Beat</h2>
|
<h2 className={styles.title}>{t('modals.binaural.title')}</h2>
|
||||||
<p className={styles.desc}>Binaural beat generator.</p>
|
<p className={styles.desc}>{t('modals.binaural.description')}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className={styles.fieldWrapper}>
|
<div className={styles.fieldWrapper}>
|
||||||
<label>
|
<label>
|
||||||
Presets:
|
{t('modals.generators.presets-label')}
|
||||||
<select value={selectedPreset} onChange={handlePresetChange}>
|
<select value={selectedPreset} onChange={handlePresetChange}>
|
||||||
{presets.map(preset => (
|
{presets.map(preset => (
|
||||||
<option key={preset.name} value={preset.name}>
|
<option key={preset.name} value={preset.name}>
|
||||||
{preset.name}
|
{t(preset.translationKey)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -183,7 +214,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||||
<>
|
<>
|
||||||
<div className={styles.fieldWrapper}>
|
<div className={styles.fieldWrapper}>
|
||||||
<label>
|
<label>
|
||||||
Base Frequency (Hz):
|
{t('modals.generators.base-frequency-label')}
|
||||||
<input
|
<input
|
||||||
max="1500"
|
max="1500"
|
||||||
min="20"
|
min="20"
|
||||||
|
|
@ -198,7 +229,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.fieldWrapper}>
|
<div className={styles.fieldWrapper}>
|
||||||
<label>
|
<label>
|
||||||
Beat Frequency (Hz):
|
{t('modals.binaural.beat-frequency-label')}
|
||||||
<input
|
<input
|
||||||
max="40"
|
max="40"
|
||||||
min="0.1"
|
min="0.1"
|
||||||
|
|
@ -213,9 +244,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.fieldWrapper}>
|
<div className={styles.fieldWrapper}>
|
||||||
<label>
|
<label>
|
||||||
Volume:
|
{t('modals.generators.volume-label')}
|
||||||
<Slider
|
<Slider
|
||||||
className={styles.volume}
|
className={styles.volume}
|
||||||
max={1}
|
max={1}
|
||||||
|
|
@ -232,10 +264,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||||
disabled={isPlaying}
|
disabled={isPlaying}
|
||||||
onClick={startSound}
|
onClick={startSound}
|
||||||
>
|
>
|
||||||
Start
|
{t('common.start')}
|
||||||
</button>
|
</button>
|
||||||
<button disabled={!isPlaying} onClick={stopSound}>
|
<button disabled={!isPlaying} onClick={stopSound}>
|
||||||
Stop
|
{t('common.stop')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
import { Exercise } from './exercise';
|
import { Exercise } from './exercise';
|
||||||
|
|
||||||
|
|
@ -9,9 +10,12 @@ interface TimerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BreathingExerciseModal({ onClose, show }: TimerProps) {
|
export function BreathingExerciseModal({ onClose, show }: TimerProps) {
|
||||||
|
const { t } = useTranslation(); // Get t function
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<h2 className={styles.title}>Breathing Exercise</h2>
|
<h2 className={styles.title || 'modal-title'}>
|
||||||
|
{t('modals.breathing.title')}
|
||||||
|
</h2>
|
||||||
<Exercise />
|
<Exercise />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { useTranslation } from 'react-i18next'; // Import
|
||||||
import { padNumber } from '@/helpers/number';
|
import { padNumber } from '@/helpers/number';
|
||||||
|
|
||||||
import styles from './exercise.module.css';
|
import styles from './exercise.module.css';
|
||||||
|
|
||||||
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
|
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>>> = {
|
const EXERCISE_DURATIONS: Record<Exercise, Partial<Record<Phase, number>>> = {
|
||||||
'4-7-8 Breathing': { exhale: 8, holdInhale: 7, inhale: 4 },
|
'4-7-8 Breathing': { exhale: 8, holdInhale: 7, inhale: 4 },
|
||||||
'Box Breathing': { exhale: 4, holdExhale: 4, holdInhale: 4, 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> = {
|
const PHASE_LABEL_KEYS: Record<Phase, string> = {
|
||||||
exhale: 'Exhale',
|
exhale: 'modals.breathing.phases.exhale',
|
||||||
holdExhale: 'Hold',
|
holdExhale: 'modals.breathing.phases.hold',
|
||||||
holdInhale: 'Hold',
|
holdInhale: 'modals.breathing.phases.hold',
|
||||||
inhale: 'Inhale',
|
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() {
|
export function Exercise() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [selectedExercise, setSelectedExercise] =
|
const [selectedExercise, setSelectedExercise] =
|
||||||
useState<Exercise>('4-7-8 Breathing');
|
useState<Exercise>('4-7-8 Breathing');
|
||||||
const [phaseIndex, setPhaseIndex] = useState(0);
|
const [phaseIndex, setPhaseIndex] = useState(0);
|
||||||
|
|
||||||
const phases = useMemo(
|
const phases = useMemo(
|
||||||
() => EXERCISE_PHASES[selectedExercise],
|
() => EXERCISE_PHASES[selectedExercise],
|
||||||
[selectedExercise],
|
[selectedExercise],
|
||||||
|
|
@ -92,6 +97,8 @@ export function Exercise() {
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const currentPhaseLabel = t(PHASE_LABEL_KEYS[currentPhase]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.exercise}>
|
<div className={styles.exercise}>
|
||||||
|
|
@ -105,7 +112,7 @@ export function Exercise() {
|
||||||
key={selectedExercise}
|
key={selectedExercise}
|
||||||
variants={animationVariants}
|
variants={animationVariants}
|
||||||
/>
|
/>
|
||||||
<p className={styles.phase}>{PHASE_LABELS[currentPhase]}</p>
|
<p className={styles.phase}>{currentPhaseLabel}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.selectWrapper}>
|
<div className={styles.selectWrapper}>
|
||||||
|
|
@ -114,9 +121,9 @@ export function Exercise() {
|
||||||
value={selectedExercise}
|
value={selectedExercise}
|
||||||
onChange={e => setSelectedExercise(e.target.value as Exercise)}
|
onChange={e => setSelectedExercise(e.target.value as Exercise)}
|
||||||
>
|
>
|
||||||
{Object.keys(EXERCISE_PHASES).map(exercise => (
|
{EXERCISE_SELECT_OPTIONS.map(option => (
|
||||||
<option key={exercise} value={exercise}>
|
<option key={option.value} value={option.value}>
|
||||||
{exercise}
|
{t(option.key)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
import { Slider } from '@/components/slider';
|
import { Slider } from '@/components/slider';
|
||||||
|
|
||||||
|
|
@ -14,18 +14,50 @@ interface Preset {
|
||||||
baseFrequency: number;
|
baseFrequency: number;
|
||||||
beatFrequency: number;
|
beatFrequency: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
translationKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const presets: Preset[] = [
|
const presets: Preset[] = [
|
||||||
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
|
{
|
||||||
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
|
baseFrequency: 100,
|
||||||
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
|
beatFrequency: 2,
|
||||||
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
|
name: 'Delta (Deep Sleep) 2 Hz',
|
||||||
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
|
translationKey: 'modals.generators.presets.delta',
|
||||||
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
|
},
|
||||||
|
{
|
||||||
|
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) {
|
export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default A4 note
|
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default A4 note
|
||||||
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat
|
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat
|
||||||
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
||||||
|
|
@ -164,17 +196,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<h2 className={styles.title}>Isochronic Tone</h2>
|
<h2 className={styles.title}>{t('modals.isochronic.title')}</h2>
|
||||||
<p className={styles.desc}>Isochronic tone generator.</p>
|
<p className={styles.desc}>{t('modals.isochronic.description')}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className={styles.fieldWrapper}>
|
<div className={styles.fieldWrapper}>
|
||||||
<label>
|
<label>
|
||||||
Presets:
|
{t('modals.generators.presets-label')} {/* Use common key */}
|
||||||
<select value={selectedPreset} onChange={handlePresetChange}>
|
<select value={selectedPreset} onChange={handlePresetChange}>
|
||||||
{presets.map(preset => (
|
{presets.map(preset => (
|
||||||
<option key={preset.name} value={preset.name}>
|
<option key={preset.name} value={preset.name}>
|
||||||
{preset.name}
|
{t(preset.translationKey)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -184,7 +216,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
||||||
<>
|
<>
|
||||||
<div className={styles.fieldWrapper}>
|
<div className={styles.fieldWrapper}>
|
||||||
<label>
|
<label>
|
||||||
Base Frequency (Hz):
|
{t('modals.generators.base-frequency-label')}{' '}
|
||||||
|
{/* Use common key */}
|
||||||
<input
|
<input
|
||||||
max="2000"
|
max="2000"
|
||||||
min="20"
|
min="20"
|
||||||
|
|
@ -199,7 +232,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.fieldWrapper}>
|
<div className={styles.fieldWrapper}>
|
||||||
<label>
|
<label>
|
||||||
Tone Frequency (Hz):
|
{t('modals.isochronic.tone-frequency-label')}{' '}
|
||||||
|
{/* Use isochronic specific key */}
|
||||||
<input
|
<input
|
||||||
max="40"
|
max="40"
|
||||||
min="0.1"
|
min="0.1"
|
||||||
|
|
@ -230,7 +264,7 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
||||||
)}
|
)}
|
||||||
<div className={styles.fieldWrapper}>
|
<div className={styles.fieldWrapper}>
|
||||||
<label>
|
<label>
|
||||||
Volume:
|
{t('modals.generators.volume-label')} {/* Use common key */}
|
||||||
<Slider
|
<Slider
|
||||||
className={styles.volume}
|
className={styles.volume}
|
||||||
max={1}
|
max={1}
|
||||||
|
|
@ -241,16 +275,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<button
|
<button
|
||||||
className={styles.primary}
|
className={styles.primary}
|
||||||
disabled={isPlaying}
|
disabled={isPlaying}
|
||||||
onClick={startSound}
|
onClick={startSound}
|
||||||
>
|
>
|
||||||
Start
|
{t('common.start')}
|
||||||
</button>
|
</button>
|
||||||
<button disabled={!isPlaying} onClick={stopSound}>
|
<button disabled={!isPlaying} onClick={stopSound}>
|
||||||
Stop
|
{t('common.stop')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index';
|
import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import styles from './list.module.css';
|
import styles from './list.module.css';
|
||||||
|
|
||||||
import { useSoundStore } from '@/stores/sound';
|
import { useSoundStore } from '@/stores/sound';
|
||||||
import { usePresetStore } from '@/stores/preset';
|
import { usePresetStore } from '@/stores/preset';
|
||||||
|
import { Tooltip } from '@/components/tooltip';
|
||||||
|
|
||||||
interface ListProps {
|
interface ListProps {
|
||||||
close: () => void;
|
close: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function List({ close }: ListProps) {
|
export function List({ close }: ListProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const presets = usePresetStore(state => state.presets);
|
const presets = usePresetStore(state => state.presets);
|
||||||
const changeName = usePresetStore(state => state.changeName);
|
const changeName = usePresetStore(state => state.changeName);
|
||||||
const deletePreset = usePresetStore(state => state.deletePreset);
|
const deletePreset = usePresetStore(state => state.deletePreset);
|
||||||
|
|
@ -19,34 +21,57 @@ export function List({ close }: ListProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.list}>
|
<div className={styles.list}>
|
||||||
<h3 className={styles.title}>
|
<h3 className={styles.title}>
|
||||||
Your Presets {presets.length > 0 && `(${presets.length})`}
|
{t('modals.presets.your-presets-title')}{' '}
|
||||||
|
{presets.length > 0 && `(${presets.length})`}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{!presets.length && (
|
{!presets.length && (
|
||||||
<p className={styles.empty}>You don't have any presets yet.</p>
|
<p className={styles.empty}>{t('modals.presets.empty')}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{presets.map(preset => (
|
{presets.map(preset => (
|
||||||
<div className={styles.preset} key={preset.id}>
|
<div className={styles.preset} key={preset.id}>
|
||||||
<input
|
<input
|
||||||
placeholder="Untitled"
|
placeholder={t('common.untitled')}
|
||||||
type="text"
|
type="text"
|
||||||
value={preset.label}
|
value={preset.label}
|
||||||
onChange={e => changeName(preset.id, e.target.value)}
|
onChange={e => changeName(preset.id, e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button onClick={() => deletePreset(preset.id)}>
|
<Tooltip
|
||||||
<FaRegTrashAlt />
|
showDelay={0}
|
||||||
</button>
|
content={
|
||||||
<button
|
t('modals.presets.delete-button-tooltip') || 'Delete preset'
|
||||||
className={styles.primary}
|
}
|
||||||
onClick={() => {
|
|
||||||
override(preset.sounds);
|
|
||||||
play();
|
|
||||||
close();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<FaPlay />
|
<button
|
||||||
</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();
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaPlay />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, type FormEvent } from 'react';
|
import { useState, type FormEvent } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '@/helpers/styles';
|
import { cn } from '@/helpers/styles';
|
||||||
import { useSoundStore } from '@/stores/sound';
|
import { useSoundStore } from '@/stores/sound';
|
||||||
import { usePresetStore } from '@/stores/preset';
|
import { usePresetStore } from '@/stores/preset';
|
||||||
|
|
@ -7,6 +7,7 @@ import { usePresetStore } from '@/stores/preset';
|
||||||
import styles from './new.module.css';
|
import styles from './new.module.css';
|
||||||
|
|
||||||
export function New() {
|
export function New() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
const noSelected = useSoundStore(state => state.noSelected());
|
const noSelected = useSoundStore(state => state.noSelected());
|
||||||
|
|
@ -33,7 +34,7 @@ export function New() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.new}>
|
<div className={styles.new}>
|
||||||
<h3 className={styles.title}>New Preset</h3>
|
<h3 className={styles.title}>{t('modals.presets.new-preset-title')}</h3>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
className={cn(styles.form, noSelected && styles.disabled)}
|
className={cn(styles.form, noSelected && styles.disabled)}
|
||||||
|
|
@ -41,18 +42,18 @@ export function New() {
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
disabled={noSelected}
|
disabled={noSelected}
|
||||||
placeholder="Preset's Name"
|
placeholder={t('modals.presets.placeholder')}
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => setName(e.target.value)}
|
onChange={e => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button disabled={noSelected}>Save</button>
|
<button disabled={noSelected}>{t('common.save')}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{noSelected && (
|
{noSelected && (
|
||||||
<p className={styles.noSelected}>
|
<p className={styles.noSelected}>
|
||||||
To make a preset, first select some sounds.
|
{t('modals.presets.no-selected-warning')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
import { New } from './new';
|
import { New } from './new';
|
||||||
import { List } from './list';
|
import { List } from './list';
|
||||||
|
|
@ -10,9 +11,11 @@ interface PresetsModalProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PresetsModal({ onClose, show }: PresetsModalProps) {
|
export function PresetsModal({ onClose, show }: PresetsModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<h2 className={styles.title}>Presets</h2>
|
<h2 className={styles.title}>{t('modals.presets.title')}</h2>
|
||||||
<New />
|
<New />
|
||||||
<div className={styles.divider} />
|
<div className={styles.divider} />
|
||||||
<List close={onClose} />
|
<List close={onClose} />
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { useMemo, useEffect, useState } from 'react';
|
import { useMemo, useEffect, useState } from 'react';
|
||||||
import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index';
|
import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
|
|
||||||
import { useCopy } from '@/hooks/use-copy';
|
import { useCopy } from '@/hooks/use-copy';
|
||||||
import { useSoundStore } from '@/stores/sound';
|
import { useSoundStore } from '@/stores/sound';
|
||||||
|
|
||||||
import styles from './share-link.module.css';
|
import styles from './share-link.module.css';
|
||||||
|
import { Tooltip } from '@/components/tooltip'; // Import Tooltip
|
||||||
|
|
||||||
interface ShareLinkModalProps {
|
interface ShareLinkModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -14,6 +15,7 @@ interface ShareLinkModalProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
|
export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const sounds = useSoundStore(state => state.sounds);
|
const sounds = useSoundStore(state => state.sounds);
|
||||||
const { copy, copying } = useCopy();
|
const { copy, copying } = useCopy();
|
||||||
|
|
@ -51,16 +53,25 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<h1 className={styles.heading}>Share your sound selection!</h1>
|
<h1 className={styles.heading}>{t('modals.share-link.title')}</h1>
|
||||||
<p className={styles.desc}>
|
<p className={styles.desc}>{t('modals.share-link.description')}</p>
|
||||||
Copy and send the following link to the person you want to share your
|
|
||||||
selection with.
|
|
||||||
</p>
|
|
||||||
<div className={styles.inputWrapper}>
|
<div className={styles.inputWrapper}>
|
||||||
<input readOnly type="text" value={url} />
|
<input readOnly type="text" value={url} />
|
||||||
<button onClick={() => copy(url)}>
|
<Tooltip
|
||||||
{copying ? <IoCheckmark /> : <IoCopyOutline />}
|
content={copying ? t('common.copied') : t('common.copy')}
|
||||||
</button>
|
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>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
|
|
||||||
import { useSoundStore } from '@/stores/sound';
|
import { useSoundStore } from '@/stores/sound';
|
||||||
|
|
@ -11,14 +11,16 @@ import { sounds } from '@/data/sounds';
|
||||||
import styles from './shared.module.css';
|
import styles from './shared.module.css';
|
||||||
|
|
||||||
export function SharedModal() {
|
export function SharedModal() {
|
||||||
|
const { t } = useTranslation(); // Get t function
|
||||||
const override = useSoundStore(state => state.override);
|
const override = useSoundStore(state => state.override);
|
||||||
const showSnackbar = useSnackbar();
|
const showSnackbar = useSnackbar();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const [sharedSounds, setSharedSounds] = useState<
|
const [sharedSounds, setSharedSounds] = useState<
|
||||||
Array<{
|
Array<{
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
labelKey: string;
|
||||||
volume: number;
|
volume: number;
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
@ -30,26 +32,26 @@ export function SharedModal() {
|
||||||
if (share) {
|
if (share) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(decodeURIComponent(share));
|
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 => {
|
sounds.categories.forEach(category => {
|
||||||
category.sounds.forEach(sound => {
|
category.sounds.forEach(sound => {
|
||||||
allSounds[sound.id] = sound.label;
|
allSoundLabelKeys[sound.id] = sound.labelKey; // Get the labelKey
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const _sharedSounds: Array<{
|
const _sharedSounds: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
labelKey: string;
|
||||||
volume: number;
|
volume: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
Object.keys(parsed).forEach(soundId => {
|
||||||
Object.keys(parsed).forEach(sound => {
|
// Check if the soundId exists and has a labelKey
|
||||||
if (allSounds[sound]) {
|
if (allSoundLabelKeys[soundId]) {
|
||||||
_sharedSounds.push({
|
_sharedSounds.push({
|
||||||
id: sound,
|
id: soundId,
|
||||||
label: allSounds[sound],
|
labelKey: allSoundLabelKeys[soundId], // Store the key
|
||||||
volume: Number(parsed[sound]),
|
volume: Number(parsed[soundId]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -59,12 +61,13 @@ export function SharedModal() {
|
||||||
setSharedSounds(_sharedSounds);
|
setSharedSounds(_sharedSounds);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return;
|
console.error('Error parsing shared URL:', error); // Log error
|
||||||
|
return; // Stop execution if parsing fails
|
||||||
} finally {
|
} finally {
|
||||||
history.pushState({}, '', location.href.split('?')[0]);
|
history.pushState({}, '', location.href.split('?')[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []); // Run only once on mount
|
||||||
|
|
||||||
const handleOverride = () => {
|
const handleOverride = () => {
|
||||||
const newSounds: Record<string, number> = {};
|
const newSounds: Record<string, number> = {};
|
||||||
|
|
@ -75,34 +78,31 @@ export function SharedModal() {
|
||||||
|
|
||||||
override(newSounds);
|
override(newSounds);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
showSnackbar('Done! You can now play the new selection.');
|
showSnackbar(t('modals.shared.snackbar-message'));
|
||||||
};
|
};
|
||||||
|
|
||||||
useCloseListener(() => setIsOpen(false));
|
useCloseListener(() => setIsOpen(false));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={isOpen} onClose={() => setIsOpen(false)}>
|
<Modal show={isOpen} onClose={() => setIsOpen(false)}>
|
||||||
<h1 className={styles.heading}>New sound mix detected!</h1>
|
<h1 className={styles.heading}>{t('modals.shared.title')}</h1>
|
||||||
<p className={styles.desc}>
|
<p className={styles.desc}>{t('modals.shared.description')}</p>
|
||||||
Someone has shared the following mix with you. Would you want to
|
|
||||||
override your current selection?
|
|
||||||
</p>
|
|
||||||
<div className={styles.sounds}>
|
<div className={styles.sounds}>
|
||||||
{sharedSounds.map(sound => (
|
{sharedSounds.map(sound => (
|
||||||
<div className={styles.sound} key={sound.id}>
|
<div className={styles.sound} key={sound.id}>
|
||||||
{sound.label}
|
{t(sound.labelKey)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<button className={cn(styles.button)} onClick={() => setIsOpen(false)}>
|
<button className={cn(styles.button)} onClick={() => setIsOpen(false)}>
|
||||||
Cancel
|
{t('common.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={cn(styles.button, styles.primary)}
|
className={cn(styles.button, styles.primary)}
|
||||||
onClick={handleOverride}
|
onClick={handleOverride}
|
||||||
>
|
>
|
||||||
Override
|
{t('common.override')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,47 @@
|
||||||
import { Modal } from '@/components/modal';
|
// src/components/modals/shortcuts/shortcuts.tsx
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import styles from './shortcuts.module.css';
|
import { Modal } from '@/components/modal'; // Assuming Modal component is stable
|
||||||
|
import styles from './shortcuts.module.css'; // Assuming styles are correct
|
||||||
|
|
||||||
interface ShortcutsModalProps {
|
interface ShortcutsModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void; // Function to close the modal
|
||||||
show: boolean;
|
show: boolean; // Boolean to control modal visibility
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
|
||||||
const shortcuts = [
|
const { t } = useTranslation();
|
||||||
{
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<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}>
|
<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
|
<Row
|
||||||
key={shortcut.label}
|
key={shortcut.labelKey}
|
||||||
keys={shortcut.keys}
|
keys={shortcut.keys}
|
||||||
label={shortcut.label}
|
// Get the translated label using the defined labelKey
|
||||||
|
label={t(shortcut.labelKey)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -90,10 +68,13 @@ function Row({ keys, label }: RowProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Props for the Key component
|
||||||
interface KeyProps {
|
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) {
|
function Key({ children }: KeyProps) {
|
||||||
|
// Simple div with styling for a key
|
||||||
return <div className={styles.key}>{children}</div>;
|
return <div className={styles.key}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useState, useRef, useMemo } from 'react';
|
import { useEffect, useState, useRef, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
import { Timer } from './timer';
|
import { Timer } from './timer';
|
||||||
import { dispatch } from '@/lib/event';
|
import { dispatch } from '@/lib/event';
|
||||||
|
|
@ -15,7 +15,37 @@ interface SleepTimerModalProps {
|
||||||
show: boolean;
|
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) {
|
export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const setActive = useSleepTimerStore(state => state.set);
|
const setActive = useSleepTimerStore(state => state.set);
|
||||||
const noSelected = useSoundStore(state => state.noSelected());
|
const noSelected = useSoundStore(state => state.noSelected());
|
||||||
|
|
||||||
|
|
@ -91,21 +121,27 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<h2 className={styles.title}>Sleep Timer</h2>
|
<h2 className={styles.title}>{t('modals.sleep-timer.title')}</h2>
|
||||||
<p className={styles.desc}>
|
<p className={styles.desc}>{t('modals.sleep-timer.description')}</p>
|
||||||
Stop sounds after a certain amount of time.
|
|
||||||
</p>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
<div className={styles.inputs}>
|
<div className={styles.inputs}>
|
||||||
{!running && (
|
{!running && (
|
||||||
<Field label="Hours" value={hours} onChange={setHours} />
|
<Field
|
||||||
|
labelKey="modals.sleep-timer.hours-label"
|
||||||
|
value={hours}
|
||||||
|
onChange={setHours}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!running && (
|
{!running && (
|
||||||
<Field label="Minutes" value={minutes} onChange={setMinutes} />
|
<Field
|
||||||
|
labelKey="modals.sleep-timer.minutes-label"
|
||||||
|
value={minutes}
|
||||||
|
onChange={setMinutes}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -118,7 +154,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
>
|
>
|
||||||
Reset
|
{t('common.reset')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -127,7 +163,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
||||||
className={cn(styles.button, styles.primary)}
|
className={cn(styles.button, styles.primary)}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Start
|
{t('common.start')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,29 +172,3 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
||||||
</Modal>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { FaHeadphonesAlt } from 'react-icons/fa/index';
|
import { FaHeadphonesAlt } from 'react-icons/fa/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
interface BinauralProps {
|
interface BinauralProps {
|
||||||
|
|
@ -7,7 +7,13 @@ interface BinauralProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Binaural({ open }: BinauralProps) {
|
export function Binaural({ open }: BinauralProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item icon={<FaHeadphonesAlt />} label="Binaural Beats" onClick={open} />
|
<Item
|
||||||
|
icon={<FaHeadphonesAlt />}
|
||||||
|
label={t('toolbar.items.binaural')}
|
||||||
|
onClick={open}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { IoMdFlower } from 'react-icons/io/index';
|
import { IoMdFlower } from 'react-icons/io/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
interface BreathingExerciseProps {
|
interface BreathingExerciseProps {
|
||||||
|
|
@ -7,10 +7,12 @@ interface BreathingExerciseProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BreathingExercise({ open }: BreathingExerciseProps) {
|
export function BreathingExercise({ open }: BreathingExerciseProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
icon={<IoMdFlower />}
|
icon={<IoMdFlower />}
|
||||||
label="Breathing Exercise"
|
label={t('toolbar.items.breathing')}
|
||||||
shortcut="Shift + B"
|
shortcut="Shift + B"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { MdOutlineTimer } from 'react-icons/md/index';
|
import { MdOutlineTimer } from 'react-icons/md/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
interface CountdownProps {
|
interface CountdownProps {
|
||||||
|
|
@ -7,10 +7,12 @@ interface CountdownProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Countdown({ open }: CountdownProps) {
|
export function Countdown({ open }: CountdownProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
icon={<MdOutlineTimer />}
|
icon={<MdOutlineTimer />}
|
||||||
label="Countdown Timer"
|
label={t('toolbar.items.countdown')}
|
||||||
shortcut="Shift + C"
|
shortcut="Shift + C"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { SiBuymeacoffee } from 'react-icons/si/index';
|
import { SiBuymeacoffee } from 'react-icons/si/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
export function Donate() {
|
export function Donate() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
href="https://buymeacoffee.com/remvze"
|
href="https://buymeacoffee.com/remvze"
|
||||||
icon={<SiBuymeacoffee />}
|
icon={<SiBuymeacoffee />} // Icon
|
||||||
label="Buy Me a Coffee"
|
label={t('toolbar.items.buy-me-a-coffee')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { TbWaveSine } from 'react-icons/tb/index';
|
import { TbWaveSine } from 'react-icons/tb/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
interface IsochronicProps {
|
interface IsochronicProps {
|
||||||
|
|
@ -7,5 +7,13 @@ interface IsochronicProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Isochronic({ open }: 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { MdNotes } from 'react-icons/md/index';
|
import { MdNotes } from 'react-icons/md/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
import { useNoteStore } from '@/stores/note';
|
import { useNoteStore } from '@/stores/note';
|
||||||
|
|
@ -9,13 +9,14 @@ interface NotepadProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Notepad({ open }: NotepadProps) {
|
export function Notepad({ open }: NotepadProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const note = useNoteStore(state => state.note);
|
const note = useNoteStore(state => state.note);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
active={!!note.length}
|
active={!!note.length}
|
||||||
icon={<MdNotes />}
|
icon={<MdNotes />}
|
||||||
label="Notepad"
|
label={t('toolbar.items.notepad')}
|
||||||
shortcut="Shift + N"
|
shortcut="Shift + N"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { MdOutlineAvTimer } from 'react-icons/md/index';
|
import { MdOutlineAvTimer } from 'react-icons/md/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
import { usePomodoroStore } from '@/stores/pomodoro';
|
import { usePomodoroStore } from '@/stores/pomodoro';
|
||||||
|
|
||||||
interface PomodoroProps {
|
interface PomodoroProps {
|
||||||
|
|
@ -9,13 +8,14 @@ interface PomodoroProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Pomodoro({ open }: PomodoroProps) {
|
export function Pomodoro({ open }: PomodoroProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const running = usePomodoroStore(state => state.running);
|
const running = usePomodoroStore(state => state.running);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
active={running}
|
active={running}
|
||||||
icon={<MdOutlineAvTimer />}
|
icon={<MdOutlineAvTimer />}
|
||||||
label="Pomodoro"
|
label={t('toolbar.items.pomodoro')}
|
||||||
shortcut="Shift + P"
|
shortcut="Shift + P"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { RiPlayListFill } from 'react-icons/ri/index';
|
import { RiPlayListFill } from 'react-icons/ri/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
interface PresetsProps {
|
interface PresetsProps {
|
||||||
|
|
@ -7,10 +7,11 @@ interface PresetsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Presets({ open }: PresetsProps) {
|
export function Presets({ open }: PresetsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
icon={<RiPlayListFill />}
|
icon={<RiPlayListFill />}
|
||||||
label="Your Presets"
|
label={t('toolbar.items.presets')}
|
||||||
shortcut="Shift + Alt + P"
|
shortcut="Shift + Alt + P"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { IoShareSocialSharp } from 'react-icons/io5/index';
|
import { IoShareSocialSharp } from 'react-icons/io5/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
import { useSoundStore } from '@/stores/sound';
|
import { useSoundStore } from '@/stores/sound';
|
||||||
|
|
@ -9,13 +9,14 @@ interface ShareProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Share({ open }: ShareProps) {
|
export function Share({ open }: ShareProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const noSelected = useSoundStore(state => state.noSelected());
|
const noSelected = useSoundStore(state => state.noSelected());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
disabled={noSelected}
|
disabled={noSelected}
|
||||||
icon={<IoShareSocialSharp />}
|
icon={<IoShareSocialSharp />}
|
||||||
label="Share Sounds"
|
label={t('toolbar.items.share')}
|
||||||
shortcut="Shift + S"
|
shortcut="Shift + S"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { MdKeyboardCommandKey } from 'react-icons/md/index';
|
import { MdKeyboardCommandKey } from 'react-icons/md/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
interface ShortcutsProps {
|
interface ShortcutsProps {
|
||||||
|
|
@ -7,10 +7,12 @@ interface ShortcutsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Shortcuts({ open }: ShortcutsProps) {
|
export function Shortcuts({ open }: ShortcutsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
icon={<MdKeyboardCommandKey />}
|
icon={<MdKeyboardCommandKey />}
|
||||||
label="Shortcuts"
|
label={t('toolbar.items.shortcuts')}
|
||||||
shortcut="Shift + H"
|
shortcut="Shift + H"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { BiShuffle } from 'react-icons/bi/index';
|
import { BiShuffle } from 'react-icons/bi/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSoundStore } from '@/stores/sound';
|
import { useSoundStore } from '@/stores/sound';
|
||||||
|
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
export function Shuffle() {
|
export function Shuffle() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const shuffle = useSoundStore(state => state.shuffle);
|
const shuffle = useSoundStore(state => state.shuffle);
|
||||||
const locked = useSoundStore(state => state.locked);
|
const locked = useSoundStore(state => state.locked);
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ export function Shuffle() {
|
||||||
<Item
|
<Item
|
||||||
disabled={locked}
|
disabled={locked}
|
||||||
icon={<BiShuffle />}
|
icon={<BiShuffle />}
|
||||||
label="Shuffle Sounds"
|
label={t('toolbar.items.shuffle')}
|
||||||
onClick={shuffle}
|
onClick={shuffle}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { IoMoonSharp } from 'react-icons/io5/index';
|
import { IoMoonSharp } from 'react-icons/io5/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSleepTimerStore } from '@/stores/sleep-timer';
|
import { useSleepTimerStore } from '@/stores/sleep-timer';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
|
|
@ -8,13 +8,14 @@ interface SleepTimerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SleepTimer({ open }: SleepTimerProps) {
|
export function SleepTimer({ open }: SleepTimerProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const active = useSleepTimerStore(state => state.active);
|
const active = useSleepTimerStore(state => state.active);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
active={active}
|
active={active}
|
||||||
icon={<IoMoonSharp />}
|
icon={<IoMoonSharp />}
|
||||||
label="Sleep Timer"
|
label={t('toolbar.items.sleep-timer')}
|
||||||
shortcut="Shift + Alt + T"
|
shortcut="Shift + Alt + T"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { LuGithub } from 'react-icons/lu/index';
|
import { LuGithub } from 'react-icons/lu/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
export function Source() {
|
export function Source() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
href="https://github.com/remvze/moodist"
|
href="https://github.com/remvze/moodist"
|
||||||
icon={<LuGithub />}
|
icon={<LuGithub />}
|
||||||
label="Source Code"
|
label={t('toolbar.items.source-code')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { MdTaskAlt } from 'react-icons/md/index';
|
import { MdTaskAlt } from 'react-icons/md/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
interface TodoProps {
|
interface TodoProps {
|
||||||
|
|
@ -7,10 +7,12 @@ interface TodoProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Todo({ open }: TodoProps) {
|
export function Todo({ open }: TodoProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
icon={<MdTaskAlt />}
|
icon={<MdTaskAlt />}
|
||||||
label="Todo Checklist"
|
label={t('toolbar.items.todo')}
|
||||||
shortcut="Shift + T"
|
shortcut="Shift + T"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { IoMenu, IoClose } from 'react-icons/io5/index';
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
ShuffleItem,
|
ShuffleItem,
|
||||||
ShareItem,
|
ShareItem,
|
||||||
|
|
@ -39,6 +39,7 @@ import { useCloseListener } from '@/hooks/use-close-listener';
|
||||||
import { closeModals } from '@/lib/modal';
|
import { closeModals } from '@/lib/modal';
|
||||||
|
|
||||||
export function Menu() {
|
export function Menu() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const noSelected = useSoundStore(state => state.noSelected());
|
const noSelected = useSoundStore(state => state.noSelected());
|
||||||
|
|
@ -100,7 +101,10 @@ export function Menu() {
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
|
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
|
||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger asChild>
|
||||||
<button aria-label="Menu" className={styles.menuButton}>
|
<button
|
||||||
|
aria-label={t('toolbar.menu-aria-label')}
|
||||||
|
className={styles.menuButton}
|
||||||
|
>
|
||||||
{isOpen ? <IoClose /> : <IoMenu />}
|
{isOpen ? <IoClose /> : <IoMenu />}
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
@ -143,7 +147,9 @@ export function Menu() {
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div className={styles.globalVolume}>
|
<div className={styles.globalVolume}>
|
||||||
<label htmlFor="global-volume">Global Volume</label>
|
<label htmlFor="global-volume">
|
||||||
|
{t('toolbar.global-volume-label')}
|
||||||
|
</label>
|
||||||
<Slider
|
<Slider
|
||||||
max={100}
|
max={100}
|
||||||
min={0}
|
min={0}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
|
|
||||||
import { useSoundEffect } from '@/hooks/use-sound-effect';
|
import { useSoundEffect } from '@/hooks/use-sound-effect';
|
||||||
|
|
@ -14,6 +14,7 @@ interface CountdownProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Countdown({ onClose, show }: CountdownProps) {
|
export function Countdown({ onClose, show }: CountdownProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [hours, setHours] = useState(0);
|
const [hours, setHours] = useState(0);
|
||||||
const [minutes, setMinutes] = useState(0);
|
const [minutes, setMinutes] = useState(0);
|
||||||
const [seconds, setSeconds] = useState(0);
|
const [seconds, setSeconds] = useState(0);
|
||||||
|
|
@ -73,8 +74,8 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<h2 className={styles.title}>Countdown Timer</h2>
|
<h2 className={styles.title}>{t('modals.countdown.title')}</h2>
|
||||||
<p className={styles.desc}>Super simple countdown timer.</p>
|
<p className={styles.desc}>{t('modals.countdown.description')}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{isFormVisible ? (
|
{isFormVisible ? (
|
||||||
|
|
@ -82,21 +83,11 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<input
|
<input
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
placeholder="HH"
|
placeholder={t('modals.countdown.placeholder-hh') || 'HH'} // Placeholder
|
||||||
type="number"
|
type="number"
|
||||||
value={hours}
|
value={hours === 0 ? '' : hours} // Show empty if 0
|
||||||
onChange={e => setHours(Math.max(0, parseInt(e.target.value)))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span>:</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className={styles.input}
|
|
||||||
placeholder="MM"
|
|
||||||
type="number"
|
|
||||||
value={minutes}
|
|
||||||
onChange={e =>
|
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
|
<input
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
placeholder="SS"
|
placeholder={t('modals.countdown.placeholder-mm') || 'MM'}
|
||||||
type="number"
|
type="number"
|
||||||
value={seconds}
|
value={minutes === 0 ? '' : minutes} // Show empty if 0
|
||||||
onChange={e =>
|
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>
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<button
|
<button
|
||||||
className={cn(styles.button, styles.primary)}
|
className={cn(styles.button, styles.primary)}
|
||||||
onClick={handleStart}
|
onClick={handleStart}
|
||||||
>
|
>
|
||||||
Start
|
{t('common.start')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -128,17 +132,15 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
||||||
<p className={styles.reverse}>- {formatTime(elapsedTime)}</p>
|
<p className={styles.reverse}>- {formatTime(elapsedTime)}</p>
|
||||||
<span>{formatTime(timeLeft)}</span>
|
<span>{formatTime(timeLeft)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<button className={styles.button} onClick={handleBack}>
|
<button className={styles.button} onClick={handleBack}>
|
||||||
Back
|
{t('common.back')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={cn(styles.button, styles.primary)}
|
className={cn(styles.button, styles.primary)}
|
||||||
onClick={toggleTimer}
|
onClick={toggleTimer}
|
||||||
>
|
>
|
||||||
{isActive ? 'Pause' : 'Start'}
|
{isActive ? t('common.pause') : t('common.start')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { BiTrash } from 'react-icons/bi/index';
|
||||||
import { LuCopy, LuDownload } from 'react-icons/lu/index';
|
import { LuCopy, LuDownload } from 'react-icons/lu/index';
|
||||||
import { FaCheck } from 'react-icons/fa6/index';
|
import { FaCheck } from 'react-icons/fa6/index';
|
||||||
import { FaUndo } from 'react-icons/fa/index';
|
import { FaUndo } from 'react-icons/fa/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
|
|
||||||
|
|
@ -19,6 +19,7 @@ interface NotepadProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Notepad({ onClose, show }: NotepadProps) {
|
export function Notepad({ onClose, show }: NotepadProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const note = useNoteStore(state => state.note);
|
const note = useNoteStore(state => state.note);
|
||||||
|
|
@ -45,26 +46,42 @@ export function Notepad({ onClose, show }: NotepadProps) {
|
||||||
if (e.key === 'Escape') onClose();
|
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 (
|
return (
|
||||||
<Modal show={show} wide onClose={onClose}>
|
<Modal show={show} wide onClose={onClose}>
|
||||||
<header className={styles.header}>
|
<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}>
|
<div className={styles.buttons}>
|
||||||
<Button
|
<Button
|
||||||
icon={copying ? <FaCheck /> : <LuCopy />}
|
icon={copying ? <FaCheck /> : <LuCopy />}
|
||||||
tooltip="Copy Note"
|
tooltip={copyTooltip}
|
||||||
onClick={() => copy(note)}
|
onClick={() => copy(note)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<LuDownload />}
|
icon={<LuDownload />}
|
||||||
tooltip="Download Note"
|
tooltip={t('modals.notepad.download-tooltip')}
|
||||||
onClick={() => download('Moodit Note.txt', note)}
|
onClick={() => download('Moodist Note.txt', note)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
critical={!history}
|
critical={!history}
|
||||||
icon={history ? <FaUndo /> : <BiTrash />}
|
icon={history ? <FaUndo /> : <BiTrash />}
|
||||||
recommended={!!history}
|
recommended={!!history}
|
||||||
tooltip={history ? 'Restore Note' : 'Clear Note'}
|
tooltip={clearOrRestoreTooltip}
|
||||||
onClick={() => (history ? restore() : clear())}
|
onClick={() => (history ? restore() : clear())}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -73,7 +90,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
|
||||||
<textarea
|
<textarea
|
||||||
className={styles.textarea}
|
className={styles.textarea}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
placeholder="What is on your mind?"
|
placeholder={t('modals.notepad.placeholder')}
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
value={note}
|
value={note}
|
||||||
|
|
@ -82,8 +99,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className={styles.counter}>
|
<p className={styles.counter}>
|
||||||
{characters} character{characters !== 1 && 's'} • {words} word
|
{t('modals.notepad.counter-stats', counterOptions)}
|
||||||
{words !== 1 && 's'}
|
|
||||||
</p>
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index';
|
import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index';
|
||||||
import { IoMdSettings } from 'react-icons/io/index';
|
import { IoMdSettings } from 'react-icons/io/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
import { Button } from '../generics/button';
|
import { Button } from '../generics/button';
|
||||||
import { Timer } from './timer';
|
import { Timer } from './timer';
|
||||||
|
|
@ -12,7 +12,6 @@ import { useLocalStorage } from '@/hooks/use-local-storage';
|
||||||
import { useSoundEffect } from '@/hooks/use-sound-effect';
|
import { useSoundEffect } from '@/hooks/use-sound-effect';
|
||||||
import { usePomodoroStore } from '@/stores/pomodoro';
|
import { usePomodoroStore } from '@/stores/pomodoro';
|
||||||
import { useCloseListener } from '@/hooks/use-close-listener';
|
import { useCloseListener } from '@/hooks/use-close-listener';
|
||||||
|
|
||||||
import styles from './pomodoro.module.css';
|
import styles from './pomodoro.module.css';
|
||||||
|
|
||||||
interface PomodoroProps {
|
interface PomodoroProps {
|
||||||
|
|
@ -22,6 +21,7 @@ interface PomodoroProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [showSetting, setShowSetting] = useState(false);
|
const [showSetting, setShowSetting] = useState(false);
|
||||||
|
|
||||||
const [selectedTab, setSelectedTab] = useState('pomodoro');
|
const [selectedTab, setSelectedTab] = useState('pomodoro');
|
||||||
|
|
@ -56,11 +56,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
||||||
|
|
||||||
const tabs = useMemo(
|
const tabs = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ id: 'pomodoro', label: 'Pomodoro' },
|
{ id: 'pomodoro', label: t('modals.pomodoro.tabs.pomodoro') },
|
||||||
{ id: 'short', label: 'Break' },
|
{ id: 'short', label: t('modals.pomodoro.tabs.short-break') },
|
||||||
{ id: 'long', label: 'Long Break' },
|
{ id: 'long', label: t('modals.pomodoro.tabs.long-break') },
|
||||||
],
|
],
|
||||||
[],
|
[t],
|
||||||
);
|
);
|
||||||
|
|
||||||
useCloseListener(() => setShowSetting(false));
|
useCloseListener(() => setShowSetting(false));
|
||||||
|
|
@ -123,12 +123,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
||||||
<>
|
<>
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<h2 className={styles.title}>Pomodoro Timer</h2>
|
<h2 className={styles.title}>{t('modals.pomodoro.title')}</h2>
|
||||||
|
|
||||||
<div className={styles.button}>
|
<div className={styles.button}>
|
||||||
<Button
|
<Button
|
||||||
icon={<IoMdSettings />}
|
icon={<IoMdSettings />}
|
||||||
tooltip="Change Times"
|
tooltip={t('modals.pomodoro.settings-tooltip')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose();
|
||||||
setShowSetting(true);
|
setShowSetting(true);
|
||||||
|
|
@ -142,19 +141,21 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
||||||
|
|
||||||
<div className={styles.control}>
|
<div className={styles.control}>
|
||||||
<p className={styles.completed}>
|
<p className={styles.completed}>
|
||||||
{completions[selectedTab] || 0} completed
|
{t('modals.pomodoro.completed', {
|
||||||
|
count: completions[selectedTab] || 0,
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<Button
|
<Button
|
||||||
icon={<FaUndo />}
|
icon={<FaUndo />}
|
||||||
smallIcon
|
smallIcon
|
||||||
tooltip="Restart"
|
tooltip={t('common.restart')}
|
||||||
onClick={restart}
|
onClick={restart}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={running ? <FaPause /> : <FaPlay />}
|
icon={running ? <FaPause /> : <FaPlay />}
|
||||||
smallIcon
|
smallIcon
|
||||||
tooltip={running ? 'Pause' : 'Start'}
|
tooltip={running ? t('common.pause') : t('common.start')}
|
||||||
onClick={toggleRunning}
|
onClick={toggleRunning}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
|
|
||||||
import styles from './setting.module.css';
|
import styles from './setting.module.css';
|
||||||
|
|
@ -12,6 +12,7 @@ interface SettingProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [values, setValues] = useState<Record<string, number | string>>(times);
|
const [values, setValues] = useState<Record<string, number | string>>(times);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -46,34 +47,34 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal lockBody={false} show={show} onClose={onClose}>
|
<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}>
|
<form className={styles.form} onSubmit={handleSubmit}>
|
||||||
<Field
|
<Field
|
||||||
id="pomodoro"
|
id="pomodoro"
|
||||||
label="Pomodoro"
|
labelKey="modals.pomodoro.settings.pomodoro-label"
|
||||||
value={values.pomodoro}
|
value={values.pomodoro}
|
||||||
onChange={handleChange('pomodoro')}
|
onChange={handleChange('pomodoro')}
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
id="short"
|
id="short"
|
||||||
label="Short Break"
|
labelKey="modals.pomodoro.settings.short-break-label"
|
||||||
value={values.short}
|
value={values.short}
|
||||||
onChange={handleChange('short')}
|
onChange={handleChange('short')}
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
id="long"
|
id="long"
|
||||||
label="Long Break"
|
labelKey="modals.pomodoro.settings.long-break-label"
|
||||||
value={values.long}
|
value={values.long}
|
||||||
onChange={handleChange('long')}
|
onChange={handleChange('long')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<button type="button" onClick={handleCancel}>
|
<button type="button" onClick={handleCancel}>
|
||||||
Cancel
|
{t('common.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button className={styles.primary} type="submit">
|
<button className={styles.primary} type="submit">
|
||||||
Save
|
{t('common.save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -83,19 +84,22 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
||||||
|
|
||||||
interface FieldProps {
|
interface FieldProps {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
labelKey: string;
|
||||||
onChange: (value: number | string) => void;
|
onChange: (value: number | string) => void;
|
||||||
value: number | string;
|
value: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Field({ id, label, onChange, value }: FieldProps) {
|
function Field({ id, labelKey, onChange, value }: FieldProps) {
|
||||||
|
const { t } = useTranslation(); // 获取翻译函数
|
||||||
return (
|
return (
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<label className={styles.label} htmlFor={id}>
|
<label className={styles.label} htmlFor={id}>
|
||||||
{label} <span>(minutes)</span>
|
{t(labelKey)}{' '}
|
||||||
|
<span>({t('modals.pomodoro.settings.minutes-unit')})</span>{' '}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
|
id={id}
|
||||||
max={120}
|
max={120}
|
||||||
min={1}
|
min={1}
|
||||||
required
|
required
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useTodoStore } from '@/stores/todo';
|
import { useTodoStore } from '@/stores/todo';
|
||||||
|
|
||||||
import styles from './form.module.css';
|
import styles from './form.module.css';
|
||||||
|
|
||||||
export function Form() {
|
export function Form() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
const addTodo = useTodoStore(state => state.addTodo);
|
const addTodo = useTodoStore(state => state.addTodo);
|
||||||
|
|
@ -22,12 +23,12 @@ export function Form() {
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<input
|
<input
|
||||||
placeholder="I have to ..."
|
placeholder={t('modals.todo.add-placeholder')}
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => setValue(e.target.value)}
|
onChange={e => setValue(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button type="submit">Add</button>
|
<button type="submit">{t('modals.todo.add-button')}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
import { Form } from './form';
|
import { Form } from './form';
|
||||||
import { Todos } from './todos';
|
import { Todos } from './todos';
|
||||||
|
|
@ -10,11 +11,13 @@ interface TodoProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Todo({ onClose, show }: TodoProps) {
|
export function Todo({ onClose, show }: TodoProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<h2 className={styles.title}>Todo Checklist</h2>
|
<h2 className={styles.title}>{t('modals.todo.title')}</h2>
|
||||||
<p className={styles.desc}>Super simple todo list.</p>
|
<p className={styles.desc}>{t('modals.todo.description')}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Form />
|
<Form />
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { FaRegTrashAlt } from 'react-icons/fa/index';
|
import { FaRegTrashAlt } from 'react-icons/fa/index';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Checkbox } from '@/components/checkbox';
|
import { Checkbox } from '@/components/checkbox';
|
||||||
|
|
||||||
import { useTodoStore } from '@/stores/todo';
|
import { useTodoStore } from '@/stores/todo';
|
||||||
import { cn } from '@/helpers/styles';
|
import { cn } from '@/helpers/styles';
|
||||||
|
|
||||||
import styles from './todo.module.css';
|
import styles from './todo.module.css';
|
||||||
|
import { Tooltip } from '@/components/tooltip';
|
||||||
|
|
||||||
interface TodoProps {
|
interface TodoProps {
|
||||||
done: boolean;
|
done: boolean;
|
||||||
|
|
@ -14,6 +15,7 @@ interface TodoProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Todo({ done, id, todo }: TodoProps) {
|
export function Todo({ done, id, todo }: TodoProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const deleteTodo = useTodoStore(state => state.deleteTodo);
|
const deleteTodo = useTodoStore(state => state.deleteTodo);
|
||||||
const toggleTodo = useTodoStore(state => state.toggleTodo);
|
const toggleTodo = useTodoStore(state => state.toggleTodo);
|
||||||
const editTodo = useTodoStore(state => state.editTodo);
|
const editTodo = useTodoStore(state => state.editTodo);
|
||||||
|
|
@ -32,9 +34,17 @@ export function Todo({ done, id, todo }: TodoProps) {
|
||||||
value={todo}
|
value={todo}
|
||||||
onChange={e => editTodo(id, e.target.value)}
|
onChange={e => editTodo(id, e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button className={styles.delete} onClick={handleDelete}>
|
<Tooltip content={t('common.delete')} showDelay={0}>
|
||||||
<FaRegTrashAlt />
|
<button
|
||||||
</button>
|
className={styles.delete}
|
||||||
|
aria-label={
|
||||||
|
t('modals.todo.delete-button-aria-label') || `Delete todo: ${todo}`
|
||||||
|
}
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<FaRegTrashAlt />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Todo } from './todo';
|
import { Todo } from './todo';
|
||||||
|
|
||||||
import { useTodoStore } from '@/stores/todo';
|
import { useTodoStore } from '@/stores/todo';
|
||||||
|
|
@ -5,13 +6,14 @@ import { useTodoStore } from '@/stores/todo';
|
||||||
import styles from './todos.module.css';
|
import styles from './todos.module.css';
|
||||||
|
|
||||||
export function Todos() {
|
export function Todos() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const todos = useTodoStore(state => state.todos);
|
const todos = useTodoStore(state => state.todos);
|
||||||
const doneCount = useTodoStore(state => state.doneCount());
|
const doneCount = useTodoStore(state => state.doneCount());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.todos}>
|
<div className={styles.todos}>
|
||||||
<header>
|
<header>
|
||||||
<p className={styles.label}>Your Todos</p>
|
<p className={styles.label}>{t('modals.todo.your-todos-label')}</p>
|
||||||
<div className={styles.divider} />
|
<div className={styles.divider} />
|
||||||
<p className={styles.counter}>
|
<p className={styles.counter}>
|
||||||
{doneCount} / {todos.length}
|
{doneCount} / {todos.length}
|
||||||
|
|
@ -30,7 +32,7 @@ export function Todos() {
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className={styles.empty}>You don't have any todos.</p>
|
<p className={styles.empty}>{t('modals.todo.empty')}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,156 @@
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"zh": "简体中文"
|
"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": {
|
"modals": {
|
||||||
"reload": {
|
"presets": {
|
||||||
"title": "New Content",
|
"title": "Presets",
|
||||||
"description": "New content available, click on reload button to update.",
|
"your-presets-title": "Your Presets",
|
||||||
"reloadButton": "Reload"
|
"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": {
|
"buttons": {
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,156 @@
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"zh": "简体中文"
|
"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": {
|
"modals": {
|
||||||
"reload": {
|
"presets": {
|
||||||
"title": "发现新内容",
|
"title": "预设",
|
||||||
"description": "检测到可用新内容,点击“重新加载”按钮进行更新。",
|
"your-presets-title": "您的预设",
|
||||||
"reloadButton": "重新加载"
|
"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": {
|
"buttons": {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue