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 { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Slider } from '@/components/slider';
|
||||
|
||||
|
|
@ -14,15 +14,46 @@ interface Preset {
|
|||
baseFrequency: number;
|
||||
beatFrequency: number;
|
||||
name: string;
|
||||
translationKey: string;
|
||||
}
|
||||
|
||||
const presets: Preset[] = [
|
||||
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
|
||||
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 2,
|
||||
name: 'Delta (Deep Sleep) 2 Hz',
|
||||
translationKey: 'modals.generators.presets.delta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 5,
|
||||
name: 'Theta (Meditation) 5 Hz',
|
||||
translationKey: 'modals.generators.presets.theta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 10,
|
||||
name: 'Alpha (Relaxation) 10 Hz',
|
||||
translationKey: 'modals.generators.presets.alpha',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 20,
|
||||
name: 'Beta (Focus) 20 Hz',
|
||||
translationKey: 'modals.generators.presets.beta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 40,
|
||||
name: 'Gamma (Cognition) 40 Hz',
|
||||
translationKey: 'modals.generators.presets.gamma',
|
||||
},
|
||||
{
|
||||
baseFrequency: 440,
|
||||
beatFrequency: 10,
|
||||
name: 'Custom',
|
||||
translationKey: 'modals.generators.presets.custom',
|
||||
},
|
||||
];
|
||||
|
||||
function computeBinauralBeatOscillatorFrequencies(
|
||||
|
|
@ -36,6 +67,7 @@ function computeBinauralBeatOscillatorFrequencies(
|
|||
}
|
||||
|
||||
export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||
const { t } = useTranslation();
|
||||
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default to A4 note
|
||||
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference
|
||||
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
||||
|
|
@ -145,15 +177,14 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
}, [selectedPreset]);
|
||||
|
||||
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selected = e.target.value;
|
||||
setSelectedPreset(selected);
|
||||
const selectedName = e.target.value;
|
||||
setSelectedPreset(selectedName);
|
||||
|
||||
if (selected === 'Custom') {
|
||||
// Allow user to input custom frequencies
|
||||
if (selectedName === 'Custom') {
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = presets.find(p => p.name === selected);
|
||||
const preset = presets.find(p => p.name === selectedName);
|
||||
if (preset) {
|
||||
setBaseFrequency(preset.baseFrequency);
|
||||
setBeatFrequency(preset.beatFrequency);
|
||||
|
|
@ -163,17 +194,17 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Binaural Beat</h2>
|
||||
<p className={styles.desc}>Binaural beat generator.</p>
|
||||
<h2 className={styles.title}>{t('modals.binaural.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.binaural.description')}</p>
|
||||
</header>
|
||||
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Presets:
|
||||
{t('modals.generators.presets-label')}
|
||||
<select value={selectedPreset} onChange={handlePresetChange}>
|
||||
{presets.map(preset => (
|
||||
<option key={preset.name} value={preset.name}>
|
||||
{preset.name}
|
||||
{t(preset.translationKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -183,7 +214,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
<>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Base Frequency (Hz):
|
||||
{t('modals.generators.base-frequency-label')}
|
||||
<input
|
||||
max="1500"
|
||||
min="20"
|
||||
|
|
@ -198,7 +229,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
</div>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Beat Frequency (Hz):
|
||||
{t('modals.binaural.beat-frequency-label')}
|
||||
<input
|
||||
max="40"
|
||||
min="0.1"
|
||||
|
|
@ -213,9 +244,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Volume:
|
||||
{t('modals.generators.volume-label')}
|
||||
<Slider
|
||||
className={styles.volume}
|
||||
max={1}
|
||||
|
|
@ -232,10 +264,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
disabled={isPlaying}
|
||||
onClick={startSound}
|
||||
>
|
||||
Start
|
||||
{t('common.start')}
|
||||
</button>
|
||||
<button disabled={!isPlaying} onClick={stopSound}>
|
||||
Stop
|
||||
{t('common.stop')}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Exercise } from './exercise';
|
||||
|
||||
|
|
@ -9,9 +10,12 @@ interface TimerProps {
|
|||
}
|
||||
|
||||
export function BreathingExerciseModal({ onClose, show }: TimerProps) {
|
||||
const { t } = useTranslation(); // Get t function
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h2 className={styles.title}>Breathing Exercise</h2>
|
||||
<h2 className={styles.title || 'modal-title'}>
|
||||
{t('modals.breathing.title')}
|
||||
</h2>
|
||||
<Exercise />
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { useTranslation } from 'react-i18next'; // Import
|
||||
import { padNumber } from '@/helpers/number';
|
||||
|
||||
import styles from './exercise.module.css';
|
||||
|
||||
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
|
||||
|
|
@ -17,21 +16,27 @@ const EXERCISE_PHASES: Record<Exercise, Phase[]> = {
|
|||
const EXERCISE_DURATIONS: Record<Exercise, Partial<Record<Phase, number>>> = {
|
||||
'4-7-8 Breathing': { exhale: 8, holdInhale: 7, inhale: 4 },
|
||||
'Box Breathing': { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 },
|
||||
'Resonant Breathing': { exhale: 5, inhale: 5 }, // No holdExhale
|
||||
'Resonant Breathing': { exhale: 5, inhale: 5 },
|
||||
};
|
||||
|
||||
const PHASE_LABELS: Record<Phase, string> = {
|
||||
exhale: 'Exhale',
|
||||
holdExhale: 'Hold',
|
||||
holdInhale: 'Hold',
|
||||
inhale: 'Inhale',
|
||||
const PHASE_LABEL_KEYS: Record<Phase, string> = {
|
||||
exhale: 'modals.breathing.phases.exhale',
|
||||
holdExhale: 'modals.breathing.phases.hold',
|
||||
holdInhale: 'modals.breathing.phases.hold',
|
||||
inhale: 'modals.breathing.phases.inhale',
|
||||
};
|
||||
|
||||
const EXERCISE_SELECT_OPTIONS: { key: string; value: Exercise }[] = [
|
||||
{ key: 'modals.breathing.exercises.478', value: '4-7-8 Breathing' },
|
||||
{ key: 'modals.breathing.exercises.box', value: 'Box Breathing' },
|
||||
{ key: 'modals.breathing.exercises.resonant', value: 'Resonant Breathing' },
|
||||
];
|
||||
|
||||
export function Exercise() {
|
||||
const { t } = useTranslation();
|
||||
const [selectedExercise, setSelectedExercise] =
|
||||
useState<Exercise>('4-7-8 Breathing');
|
||||
const [phaseIndex, setPhaseIndex] = useState(0);
|
||||
|
||||
const phases = useMemo(
|
||||
() => EXERCISE_PHASES[selectedExercise],
|
||||
[selectedExercise],
|
||||
|
|
@ -92,6 +97,8 @@ export function Exercise() {
|
|||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const currentPhaseLabel = t(PHASE_LABEL_KEYS[currentPhase]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.exercise}>
|
||||
|
|
@ -105,7 +112,7 @@ export function Exercise() {
|
|||
key={selectedExercise}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<p className={styles.phase}>{PHASE_LABELS[currentPhase]}</p>
|
||||
<p className={styles.phase}>{currentPhaseLabel}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.selectWrapper}>
|
||||
|
|
@ -114,9 +121,9 @@ export function Exercise() {
|
|||
value={selectedExercise}
|
||||
onChange={e => setSelectedExercise(e.target.value as Exercise)}
|
||||
>
|
||||
{Object.keys(EXERCISE_PHASES).map(exercise => (
|
||||
<option key={exercise} value={exercise}>
|
||||
{exercise}
|
||||
{EXERCISE_SELECT_OPTIONS.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{t(option.key)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Slider } from '@/components/slider';
|
||||
|
||||
|
|
@ -14,18 +14,50 @@ interface Preset {
|
|||
baseFrequency: number;
|
||||
beatFrequency: number;
|
||||
name: string;
|
||||
translationKey: string;
|
||||
}
|
||||
|
||||
const presets: Preset[] = [
|
||||
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
|
||||
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 2,
|
||||
name: 'Delta (Deep Sleep) 2 Hz',
|
||||
translationKey: 'modals.generators.presets.delta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 5,
|
||||
name: 'Theta (Meditation) 5 Hz',
|
||||
translationKey: 'modals.generators.presets.theta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 10,
|
||||
name: 'Alpha (Relaxation) 10 Hz',
|
||||
translationKey: 'modals.generators.presets.alpha',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 20,
|
||||
name: 'Beta (Focus) 20 Hz',
|
||||
translationKey: 'modals.generators.presets.beta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 40,
|
||||
name: 'Gamma (Cognition) 40 Hz',
|
||||
translationKey: 'modals.generators.presets.gamma',
|
||||
},
|
||||
{
|
||||
baseFrequency: 440,
|
||||
beatFrequency: 10,
|
||||
name: 'Custom',
|
||||
translationKey: 'modals.generators.presets.custom',
|
||||
},
|
||||
];
|
||||
|
||||
export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
||||
const { t } = useTranslation();
|
||||
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default A4 note
|
||||
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat
|
||||
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
||||
|
|
@ -164,17 +196,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Isochronic Tone</h2>
|
||||
<p className={styles.desc}>Isochronic tone generator.</p>
|
||||
<h2 className={styles.title}>{t('modals.isochronic.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.isochronic.description')}</p>
|
||||
</header>
|
||||
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Presets:
|
||||
{t('modals.generators.presets-label')} {/* Use common key */}
|
||||
<select value={selectedPreset} onChange={handlePresetChange}>
|
||||
{presets.map(preset => (
|
||||
<option key={preset.name} value={preset.name}>
|
||||
{preset.name}
|
||||
{t(preset.translationKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -184,7 +216,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
<>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Base Frequency (Hz):
|
||||
{t('modals.generators.base-frequency-label')}{' '}
|
||||
{/* Use common key */}
|
||||
<input
|
||||
max="2000"
|
||||
min="20"
|
||||
|
|
@ -199,7 +232,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
</div>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Tone Frequency (Hz):
|
||||
{t('modals.isochronic.tone-frequency-label')}{' '}
|
||||
{/* Use isochronic specific key */}
|
||||
<input
|
||||
max="40"
|
||||
min="0.1"
|
||||
|
|
@ -230,7 +264,7 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
)}
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Volume:
|
||||
{t('modals.generators.volume-label')} {/* Use common key */}
|
||||
<Slider
|
||||
className={styles.volume}
|
||||
max={1}
|
||||
|
|
@ -241,16 +275,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<button
|
||||
className={styles.primary}
|
||||
disabled={isPlaying}
|
||||
onClick={startSound}
|
||||
>
|
||||
Start
|
||||
{t('common.start')}
|
||||
</button>
|
||||
<button disabled={!isPlaying} onClick={stopSound}>
|
||||
Stop
|
||||
{t('common.stop')}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from './list.module.css';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { usePresetStore } from '@/stores/preset';
|
||||
import { Tooltip } from '@/components/tooltip';
|
||||
|
||||
interface ListProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export function List({ close }: ListProps) {
|
||||
const { t } = useTranslation();
|
||||
const presets = usePresetStore(state => state.presets);
|
||||
const changeName = usePresetStore(state => state.changeName);
|
||||
const deletePreset = usePresetStore(state => state.deletePreset);
|
||||
|
|
@ -19,34 +21,57 @@ export function List({ close }: ListProps) {
|
|||
return (
|
||||
<div className={styles.list}>
|
||||
<h3 className={styles.title}>
|
||||
Your Presets {presets.length > 0 && `(${presets.length})`}
|
||||
{t('modals.presets.your-presets-title')}{' '}
|
||||
{presets.length > 0 && `(${presets.length})`}
|
||||
</h3>
|
||||
|
||||
{!presets.length && (
|
||||
<p className={styles.empty}>You don't have any presets yet.</p>
|
||||
<p className={styles.empty}>{t('modals.presets.empty')}</p>
|
||||
)}
|
||||
|
||||
{presets.map(preset => (
|
||||
<div className={styles.preset} key={preset.id}>
|
||||
<input
|
||||
placeholder="Untitled"
|
||||
placeholder={t('common.untitled')}
|
||||
type="text"
|
||||
value={preset.label}
|
||||
onChange={e => changeName(preset.id, e.target.value)}
|
||||
/>
|
||||
<button onClick={() => deletePreset(preset.id)}>
|
||||
<FaRegTrashAlt />
|
||||
</button>
|
||||
<button
|
||||
className={styles.primary}
|
||||
onClick={() => {
|
||||
override(preset.sounds);
|
||||
play();
|
||||
close();
|
||||
}}
|
||||
<Tooltip
|
||||
showDelay={0}
|
||||
content={
|
||||
t('modals.presets.delete-button-tooltip') || 'Delete preset'
|
||||
}
|
||||
>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, type FormEvent } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/helpers/styles';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { usePresetStore } from '@/stores/preset';
|
||||
|
|
@ -7,6 +7,7 @@ import { usePresetStore } from '@/stores/preset';
|
|||
import styles from './new.module.css';
|
||||
|
||||
export function New() {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
|
@ -33,7 +34,7 @@ export function New() {
|
|||
|
||||
return (
|
||||
<div className={styles.new}>
|
||||
<h3 className={styles.title}>New Preset</h3>
|
||||
<h3 className={styles.title}>{t('modals.presets.new-preset-title')}</h3>
|
||||
|
||||
<form
|
||||
className={cn(styles.form, noSelected && styles.disabled)}
|
||||
|
|
@ -41,18 +42,18 @@ export function New() {
|
|||
>
|
||||
<input
|
||||
disabled={noSelected}
|
||||
placeholder="Preset's Name"
|
||||
placeholder={t('modals.presets.placeholder')}
|
||||
required
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
<button disabled={noSelected}>Save</button>
|
||||
<button disabled={noSelected}>{t('common.save')}</button>
|
||||
</form>
|
||||
|
||||
{noSelected && (
|
||||
<p className={styles.noSelected}>
|
||||
To make a preset, first select some sounds.
|
||||
{t('modals.presets.no-selected-warning')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { New } from './new';
|
||||
import { List } from './list';
|
||||
|
|
@ -10,9 +11,11 @@ interface PresetsModalProps {
|
|||
}
|
||||
|
||||
export function PresetsModal({ onClose, show }: PresetsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h2 className={styles.title}>Presets</h2>
|
||||
<h2 className={styles.title}>{t('modals.presets.title')}</h2>
|
||||
<New />
|
||||
<div className={styles.divider} />
|
||||
<List close={onClose} />
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { useMemo, useEffect, useState } from 'react';
|
||||
import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import { useCopy } from '@/hooks/use-copy';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
import styles from './share-link.module.css';
|
||||
import { Tooltip } from '@/components/tooltip'; // Import Tooltip
|
||||
|
||||
interface ShareLinkModalProps {
|
||||
onClose: () => void;
|
||||
|
|
@ -14,6 +15,7 @@ interface ShareLinkModalProps {
|
|||
}
|
||||
|
||||
export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const sounds = useSoundStore(state => state.sounds);
|
||||
const { copy, copying } = useCopy();
|
||||
|
|
@ -51,16 +53,25 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
|
|||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h1 className={styles.heading}>Share your sound selection!</h1>
|
||||
<p className={styles.desc}>
|
||||
Copy and send the following link to the person you want to share your
|
||||
selection with.
|
||||
</p>
|
||||
<h1 className={styles.heading}>{t('modals.share-link.title')}</h1>
|
||||
<p className={styles.desc}>{t('modals.share-link.description')}</p>
|
||||
<div className={styles.inputWrapper}>
|
||||
<input readOnly type="text" value={url} />
|
||||
<button onClick={() => copy(url)}>
|
||||
{copying ? <IoCheckmark /> : <IoCopyOutline />}
|
||||
</button>
|
||||
<Tooltip
|
||||
content={copying ? t('common.copied') : t('common.copy')}
|
||||
showDelay={0}
|
||||
>
|
||||
<button
|
||||
aria-label={
|
||||
copying
|
||||
? t('common.copied')
|
||||
: t('modals.share-link.copy-button-aria-label')
|
||||
}
|
||||
onClick={() => copy(url)}
|
||||
>
|
||||
{copying ? <IoCheckmark /> : <IoCopyOutline />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
|
@ -11,14 +11,16 @@ import { sounds } from '@/data/sounds';
|
|||
import styles from './shared.module.css';
|
||||
|
||||
export function SharedModal() {
|
||||
const { t } = useTranslation(); // Get t function
|
||||
const override = useSoundStore(state => state.override);
|
||||
const showSnackbar = useSnackbar();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [sharedSounds, setSharedSounds] = useState<
|
||||
Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
labelKey: string;
|
||||
volume: number;
|
||||
}>
|
||||
>([]);
|
||||
|
|
@ -30,26 +32,26 @@ export function SharedModal() {
|
|||
if (share) {
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURIComponent(share));
|
||||
const allSounds: Record<string, string> = {};
|
||||
|
||||
// Map sound IDs to their labelKeys for quick lookup
|
||||
const allSoundLabelKeys: Record<string, string> = {};
|
||||
sounds.categories.forEach(category => {
|
||||
category.sounds.forEach(sound => {
|
||||
allSounds[sound.id] = sound.label;
|
||||
allSoundLabelKeys[sound.id] = sound.labelKey; // Get the labelKey
|
||||
});
|
||||
});
|
||||
|
||||
const _sharedSounds: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
labelKey: string;
|
||||
volume: number;
|
||||
}> = [];
|
||||
|
||||
Object.keys(parsed).forEach(sound => {
|
||||
if (allSounds[sound]) {
|
||||
Object.keys(parsed).forEach(soundId => {
|
||||
// Check if the soundId exists and has a labelKey
|
||||
if (allSoundLabelKeys[soundId]) {
|
||||
_sharedSounds.push({
|
||||
id: sound,
|
||||
label: allSounds[sound],
|
||||
volume: Number(parsed[sound]),
|
||||
id: soundId,
|
||||
labelKey: allSoundLabelKeys[soundId], // Store the key
|
||||
volume: Number(parsed[soundId]),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -59,12 +61,13 @@ export function SharedModal() {
|
|||
setSharedSounds(_sharedSounds);
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
console.error('Error parsing shared URL:', error); // Log error
|
||||
return; // Stop execution if parsing fails
|
||||
} finally {
|
||||
history.pushState({}, '', location.href.split('?')[0]);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, []); // Run only once on mount
|
||||
|
||||
const handleOverride = () => {
|
||||
const newSounds: Record<string, number> = {};
|
||||
|
|
@ -75,34 +78,31 @@ export function SharedModal() {
|
|||
|
||||
override(newSounds);
|
||||
setIsOpen(false);
|
||||
showSnackbar('Done! You can now play the new selection.');
|
||||
showSnackbar(t('modals.shared.snackbar-message'));
|
||||
};
|
||||
|
||||
useCloseListener(() => setIsOpen(false));
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onClose={() => setIsOpen(false)}>
|
||||
<h1 className={styles.heading}>New sound mix detected!</h1>
|
||||
<p className={styles.desc}>
|
||||
Someone has shared the following mix with you. Would you want to
|
||||
override your current selection?
|
||||
</p>
|
||||
<h1 className={styles.heading}>{t('modals.shared.title')}</h1>
|
||||
<p className={styles.desc}>{t('modals.shared.description')}</p>
|
||||
<div className={styles.sounds}>
|
||||
{sharedSounds.map(sound => (
|
||||
<div className={styles.sound} key={sound.id}>
|
||||
{sound.label}
|
||||
{t(sound.labelKey)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<button className={cn(styles.button)} onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className={cn(styles.button, styles.primary)}
|
||||
onClick={handleOverride}
|
||||
>
|
||||
Override
|
||||
{t('common.override')}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,69 +1,47 @@
|
|||
import { Modal } from '@/components/modal';
|
||||
|
||||
import styles from './shortcuts.module.css';
|
||||
// src/components/modals/shortcuts/shortcuts.tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal'; // Assuming Modal component is stable
|
||||
import styles from './shortcuts.module.css'; // Assuming styles are correct
|
||||
|
||||
interface ShortcutsModalProps {
|
||||
onClose: () => void;
|
||||
show: boolean;
|
||||
onClose: () => void; // Function to close the modal
|
||||
show: boolean; // Boolean to control modal visibility
|
||||
}
|
||||
|
||||
interface ShortcutItem {
|
||||
keys: string[];
|
||||
labelKey: string;
|
||||
}
|
||||
|
||||
const shortcutsList: ShortcutItem[] = [
|
||||
{ keys: ['Shift', 'H'], labelKey: 'toolbar.items.shortcuts' }, // Reusing toolbar item key
|
||||
{ keys: ['Shift', 'Alt', 'P'], labelKey: 'toolbar.items.presets' },
|
||||
{ keys: ['Shift', 'S'], labelKey: 'toolbar.items.share' },
|
||||
{ keys: ['Shift', 'Alt', 'T'], labelKey: 'toolbar.items.sleep-timer' },
|
||||
{ keys: ['Shift', 'C'], labelKey: 'toolbar.items.countdown' },
|
||||
{ keys: ['Shift', 'P'], labelKey: 'toolbar.items.pomodoro' },
|
||||
{ keys: ['Shift', 'N'], labelKey: 'toolbar.items.notepad' },
|
||||
{ keys: ['Shift', 'T'], labelKey: 'toolbar.items.todo' },
|
||||
{ keys: ['Shift', 'B'], labelKey: 'toolbar.items.breathing' },
|
||||
{ keys: ['Shift', 'Space'], labelKey: 'modals.shortcuts.labels.toggle-play' }, // Specific key
|
||||
{ keys: ['Shift', 'R'], labelKey: 'modals.shortcuts.labels.unselect-all' }, // Specific key
|
||||
];
|
||||
|
||||
export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
|
||||
const shortcuts = [
|
||||
{
|
||||
keys: ['Shift', 'H'],
|
||||
label: 'Shortcuts List',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'Alt', 'P'],
|
||||
label: 'Presets',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'S'],
|
||||
label: 'Share Sounds',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'Alt', 'T'],
|
||||
label: 'Sleep Timer',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'C'],
|
||||
label: 'Countdown Timer',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'P'],
|
||||
label: 'Pomodoro',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'N'],
|
||||
label: 'Notepad',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'T'],
|
||||
label: 'Todo Checklist',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'B'],
|
||||
label: 'Breathing Exercise',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'Space'],
|
||||
label: 'Toggle Play',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'R'],
|
||||
label: 'Unselect All Sounds',
|
||||
},
|
||||
];
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h1 className={styles.heading}>Keyboard Shortcuts</h1>
|
||||
<h1 className={styles.heading}>{t('modals.shortcuts.title')}</h1>
|
||||
<div className={styles.shortcuts}>
|
||||
{shortcuts.map(shortcut => (
|
||||
{shortcutsList.map(shortcut => (
|
||||
// Render a Row for each shortcut item
|
||||
// Use the labelKey as the React key for stability if IDs aren't available
|
||||
<Row
|
||||
key={shortcut.label}
|
||||
key={shortcut.labelKey}
|
||||
keys={shortcut.keys}
|
||||
label={shortcut.label}
|
||||
// Get the translated label using the defined labelKey
|
||||
label={t(shortcut.labelKey)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -90,10 +68,13 @@ function Row({ keys, label }: RowProps) {
|
|||
);
|
||||
}
|
||||
|
||||
// Props for the Key component
|
||||
interface KeyProps {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode; // The text content (key name)
|
||||
}
|
||||
|
||||
// Component to render a single keyboard key representation
|
||||
function Key({ children }: KeyProps) {
|
||||
// Simple div with styling for a key
|
||||
return <div className={styles.key}>{children}</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState, useRef, useMemo } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Timer } from './timer';
|
||||
import { dispatch } from '@/lib/event';
|
||||
|
|
@ -15,7 +15,37 @@ interface SleepTimerModalProps {
|
|||
show: boolean;
|
||||
}
|
||||
|
||||
interface FieldProps {
|
||||
labelKey: string;
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function Field({ labelKey, onChange, value }: FieldProps) {
|
||||
const { t } = useTranslation();
|
||||
const label = t(labelKey);
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={labelKey}>
|
||||
{' '}
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
id={labelKey}
|
||||
max="59"
|
||||
min="0"
|
||||
required
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value === '' ? '' : e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const setActive = useSleepTimerStore(state => state.set);
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
||||
|
|
@ -91,21 +121,27 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
|||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Sleep Timer</h2>
|
||||
<p className={styles.desc}>
|
||||
Stop sounds after a certain amount of time.
|
||||
</p>
|
||||
<h2 className={styles.title}>{t('modals.sleep-timer.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.sleep-timer.description')}</p>
|
||||
</header>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.controls}>
|
||||
<div className={styles.inputs}>
|
||||
{!running && (
|
||||
<Field label="Hours" value={hours} onChange={setHours} />
|
||||
<Field
|
||||
labelKey="modals.sleep-timer.hours-label"
|
||||
value={hours}
|
||||
onChange={setHours}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!running && (
|
||||
<Field label="Minutes" value={minutes} onChange={setMinutes} />
|
||||
<Field
|
||||
labelKey="modals.sleep-timer.minutes-label"
|
||||
value={minutes}
|
||||
onChange={setMinutes}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -118,7 +154,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
|||
type="button"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
{t('common.reset')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
|
@ -127,7 +163,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
|||
className={cn(styles.button, styles.primary)}
|
||||
type="submit"
|
||||
>
|
||||
Start
|
||||
{t('common.start')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -136,29 +172,3 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
|||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface FieldProps {
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function Field({ label, onChange, value }: FieldProps) {
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={label.toLocaleLowerCase()}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
id={label.toLocaleLowerCase()}
|
||||
max="59"
|
||||
min="0"
|
||||
required
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value === '' ? '' : e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { FaHeadphonesAlt } from 'react-icons/fa/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface BinauralProps {
|
||||
|
|
@ -7,7 +7,13 @@ interface BinauralProps {
|
|||
}
|
||||
|
||||
export function Binaural({ open }: BinauralProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item icon={<FaHeadphonesAlt />} label="Binaural Beats" onClick={open} />
|
||||
<Item
|
||||
icon={<FaHeadphonesAlt />}
|
||||
label={t('toolbar.items.binaural')}
|
||||
onClick={open}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { IoMdFlower } from 'react-icons/io/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface BreathingExerciseProps {
|
||||
|
|
@ -7,10 +7,12 @@ interface BreathingExerciseProps {
|
|||
}
|
||||
|
||||
export function BreathingExercise({ open }: BreathingExerciseProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<IoMdFlower />}
|
||||
label="Breathing Exercise"
|
||||
label={t('toolbar.items.breathing')}
|
||||
shortcut="Shift + B"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MdOutlineTimer } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface CountdownProps {
|
||||
|
|
@ -7,10 +7,12 @@ interface CountdownProps {
|
|||
}
|
||||
|
||||
export function Countdown({ open }: CountdownProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<MdOutlineTimer />}
|
||||
label="Countdown Timer"
|
||||
label={t('toolbar.items.countdown')}
|
||||
shortcut="Shift + C"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { SiBuymeacoffee } from 'react-icons/si/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
export function Donate() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
href="https://buymeacoffee.com/remvze"
|
||||
icon={<SiBuymeacoffee />}
|
||||
label="Buy Me a Coffee"
|
||||
icon={<SiBuymeacoffee />} // Icon
|
||||
label={t('toolbar.items.buy-me-a-coffee')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { TbWaveSine } from 'react-icons/tb/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface IsochronicProps {
|
||||
|
|
@ -7,5 +7,13 @@ interface IsochronicProps {
|
|||
}
|
||||
|
||||
export function Isochronic({ open }: IsochronicProps) {
|
||||
return <Item icon={<TbWaveSine />} label="Isochronic Tones" onClick={open} />;
|
||||
const { t } = useTranslation(); // Get t function
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<TbWaveSine />}
|
||||
label={t('toolbar.items.isochronic')}
|
||||
onClick={open}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MdNotes } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
import { useNoteStore } from '@/stores/note';
|
||||
|
|
@ -9,13 +9,14 @@ interface NotepadProps {
|
|||
}
|
||||
|
||||
export function Notepad({ open }: NotepadProps) {
|
||||
const { t } = useTranslation();
|
||||
const note = useNoteStore(state => state.note);
|
||||
|
||||
return (
|
||||
<Item
|
||||
active={!!note.length}
|
||||
icon={<MdNotes />}
|
||||
label="Notepad"
|
||||
label={t('toolbar.items.notepad')}
|
||||
shortcut="Shift + N"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { MdOutlineAvTimer } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
import { usePomodoroStore } from '@/stores/pomodoro';
|
||||
|
||||
interface PomodoroProps {
|
||||
|
|
@ -9,13 +8,14 @@ interface PomodoroProps {
|
|||
}
|
||||
|
||||
export function Pomodoro({ open }: PomodoroProps) {
|
||||
const { t } = useTranslation();
|
||||
const running = usePomodoroStore(state => state.running);
|
||||
|
||||
return (
|
||||
<Item
|
||||
active={running}
|
||||
icon={<MdOutlineAvTimer />}
|
||||
label="Pomodoro"
|
||||
label={t('toolbar.items.pomodoro')}
|
||||
shortcut="Shift + P"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { RiPlayListFill } from 'react-icons/ri/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface PresetsProps {
|
||||
|
|
@ -7,10 +7,11 @@ interface PresetsProps {
|
|||
}
|
||||
|
||||
export function Presets({ open }: PresetsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Item
|
||||
icon={<RiPlayListFill />}
|
||||
label="Your Presets"
|
||||
label={t('toolbar.items.presets')}
|
||||
shortcut="Shift + Alt + P"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { IoShareSocialSharp } from 'react-icons/io5/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
|
@ -9,13 +9,14 @@ interface ShareProps {
|
|||
}
|
||||
|
||||
export function Share({ open }: ShareProps) {
|
||||
const { t } = useTranslation();
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
||||
return (
|
||||
<Item
|
||||
disabled={noSelected}
|
||||
icon={<IoShareSocialSharp />}
|
||||
label="Share Sounds"
|
||||
label={t('toolbar.items.share')}
|
||||
shortcut="Shift + S"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MdKeyboardCommandKey } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface ShortcutsProps {
|
||||
|
|
@ -7,10 +7,12 @@ interface ShortcutsProps {
|
|||
}
|
||||
|
||||
export function Shortcuts({ open }: ShortcutsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<MdKeyboardCommandKey />}
|
||||
label="Shortcuts"
|
||||
label={t('toolbar.items.shortcuts')}
|
||||
shortcut="Shift + H"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { BiShuffle } from 'react-icons/bi/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
export function Shuffle() {
|
||||
const { t } = useTranslation();
|
||||
const shuffle = useSoundStore(state => state.shuffle);
|
||||
const locked = useSoundStore(state => state.locked);
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ export function Shuffle() {
|
|||
<Item
|
||||
disabled={locked}
|
||||
icon={<BiShuffle />}
|
||||
label="Shuffle Sounds"
|
||||
label={t('toolbar.items.shuffle')}
|
||||
onClick={shuffle}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { IoMoonSharp } from 'react-icons/io5/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSleepTimerStore } from '@/stores/sleep-timer';
|
||||
import { Item } from '../item';
|
||||
|
||||
|
|
@ -8,13 +8,14 @@ interface SleepTimerProps {
|
|||
}
|
||||
|
||||
export function SleepTimer({ open }: SleepTimerProps) {
|
||||
const { t } = useTranslation();
|
||||
const active = useSleepTimerStore(state => state.active);
|
||||
|
||||
return (
|
||||
<Item
|
||||
active={active}
|
||||
icon={<IoMoonSharp />}
|
||||
label="Sleep Timer"
|
||||
label={t('toolbar.items.sleep-timer')}
|
||||
shortcut="Shift + Alt + T"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { LuGithub } from 'react-icons/lu/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
export function Source() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
href="https://github.com/remvze/moodist"
|
||||
icon={<LuGithub />}
|
||||
label="Source Code"
|
||||
label={t('toolbar.items.source-code')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MdTaskAlt } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface TodoProps {
|
||||
|
|
@ -7,10 +7,12 @@ interface TodoProps {
|
|||
}
|
||||
|
||||
export function Todo({ open }: TodoProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<MdTaskAlt />}
|
||||
label="Todo Checklist"
|
||||
label={t('toolbar.items.todo')}
|
||||
shortcut="Shift + T"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { IoMenu, IoClose } from 'react-icons/io5/index';
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ShuffleItem,
|
||||
ShareItem,
|
||||
|
|
@ -39,6 +39,7 @@ import { useCloseListener } from '@/hooks/use-close-listener';
|
|||
import { closeModals } from '@/lib/modal';
|
||||
|
||||
export function Menu() {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
|
@ -100,7 +101,10 @@ export function Menu() {
|
|||
<div className={styles.wrapper}>
|
||||
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button aria-label="Menu" className={styles.menuButton}>
|
||||
<button
|
||||
aria-label={t('toolbar.menu-aria-label')}
|
||||
className={styles.menuButton}
|
||||
>
|
||||
{isOpen ? <IoClose /> : <IoMenu />}
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
|
@ -143,7 +147,9 @@ export function Menu() {
|
|||
<Divider />
|
||||
|
||||
<div className={styles.globalVolume}>
|
||||
<label htmlFor="global-volume">Global Volume</label>
|
||||
<label htmlFor="global-volume">
|
||||
{t('toolbar.global-volume-label')}
|
||||
</label>
|
||||
<Slider
|
||||
max={100}
|
||||
min={0}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import { useSoundEffect } from '@/hooks/use-sound-effect';
|
||||
|
|
@ -14,6 +14,7 @@ interface CountdownProps {
|
|||
}
|
||||
|
||||
export function Countdown({ onClose, show }: CountdownProps) {
|
||||
const { t } = useTranslation();
|
||||
const [hours, setHours] = useState(0);
|
||||
const [minutes, setMinutes] = useState(0);
|
||||
const [seconds, setSeconds] = useState(0);
|
||||
|
|
@ -73,8 +74,8 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
|||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Countdown Timer</h2>
|
||||
<p className={styles.desc}>Super simple countdown timer.</p>
|
||||
<h2 className={styles.title}>{t('modals.countdown.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.countdown.description')}</p>
|
||||
</header>
|
||||
|
||||
{isFormVisible ? (
|
||||
|
|
@ -82,21 +83,11 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
|||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
className={styles.input}
|
||||
placeholder="HH"
|
||||
placeholder={t('modals.countdown.placeholder-hh') || 'HH'} // Placeholder
|
||||
type="number"
|
||||
value={hours}
|
||||
onChange={e => setHours(Math.max(0, parseInt(e.target.value)))}
|
||||
/>
|
||||
|
||||
<span>:</span>
|
||||
|
||||
<input
|
||||
className={styles.input}
|
||||
placeholder="MM"
|
||||
type="number"
|
||||
value={minutes}
|
||||
value={hours === 0 ? '' : hours} // Show empty if 0
|
||||
onChange={e =>
|
||||
setMinutes(Math.max(0, Math.min(59, parseInt(e.target.value))))
|
||||
setHours(Math.max(0, parseInt(e.target.value || '0')))
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
@ -104,21 +95,34 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
|||
|
||||
<input
|
||||
className={styles.input}
|
||||
placeholder="SS"
|
||||
placeholder={t('modals.countdown.placeholder-mm') || 'MM'}
|
||||
type="number"
|
||||
value={seconds}
|
||||
value={minutes === 0 ? '' : minutes} // Show empty if 0
|
||||
onChange={e =>
|
||||
setSeconds(Math.max(0, Math.min(59, parseInt(e.target.value))))
|
||||
setMinutes(
|
||||
Math.max(0, Math.min(59, parseInt(e.target.value || '0'))),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span>:</span>
|
||||
<input
|
||||
className={styles.input}
|
||||
placeholder={t('modals.countdown.placeholder-ss') || 'SS'}
|
||||
type="number"
|
||||
value={seconds === 0 ? '' : seconds} // Show empty if 0
|
||||
onChange={e =>
|
||||
setSeconds(
|
||||
Math.max(0, Math.min(59, parseInt(e.target.value || '0'))),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<button
|
||||
className={cn(styles.button, styles.primary)}
|
||||
onClick={handleStart}
|
||||
>
|
||||
Start
|
||||
{t('common.start')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -128,17 +132,15 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
|||
<p className={styles.reverse}>- {formatTime(elapsedTime)}</p>
|
||||
<span>{formatTime(timeLeft)}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<button className={styles.button} onClick={handleBack}>
|
||||
Back
|
||||
{t('common.back')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={cn(styles.button, styles.primary)}
|
||||
onClick={toggleTimer}
|
||||
>
|
||||
{isActive ? 'Pause' : 'Start'}
|
||||
{isActive ? t('common.pause') : t('common.start')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { BiTrash } from 'react-icons/bi/index';
|
|||
import { LuCopy, LuDownload } from 'react-icons/lu/index';
|
||||
import { FaCheck } from 'react-icons/fa6/index';
|
||||
import { FaUndo } from 'react-icons/fa/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Button } from './button';
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ interface NotepadProps {
|
|||
}
|
||||
|
||||
export function Notepad({ onClose, show }: NotepadProps) {
|
||||
const { t } = useTranslation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const note = useNoteStore(state => state.note);
|
||||
|
|
@ -45,26 +46,42 @@ export function Notepad({ onClose, show }: NotepadProps) {
|
|||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
const counterOptions = {
|
||||
chars: characters,
|
||||
chars_plural:
|
||||
characters !== 1 ? t('common.plural-suffix', { defaultValue: 's' }) : '',
|
||||
words: words,
|
||||
words_plural:
|
||||
words !== 1 ? t('common.plural-suffix', { defaultValue: 's' }) : '',
|
||||
};
|
||||
|
||||
const clearOrRestoreTooltip = history
|
||||
? t('modals.notepad.restore-tooltip')
|
||||
: t('modals.notepad.clear-tooltip');
|
||||
const copyTooltip = copying
|
||||
? t('common.copied')
|
||||
: t('modals.notepad.copy-tooltip');
|
||||
|
||||
return (
|
||||
<Modal show={show} wide onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.label}>Your Note</h2>
|
||||
<h2 className={styles.label}>{t('modals.notepad.title-label')}</h2>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
icon={copying ? <FaCheck /> : <LuCopy />}
|
||||
tooltip="Copy Note"
|
||||
tooltip={copyTooltip}
|
||||
onClick={() => copy(note)}
|
||||
/>
|
||||
<Button
|
||||
icon={<LuDownload />}
|
||||
tooltip="Download Note"
|
||||
onClick={() => download('Moodit Note.txt', note)}
|
||||
tooltip={t('modals.notepad.download-tooltip')}
|
||||
onClick={() => download('Moodist Note.txt', note)}
|
||||
/>
|
||||
<Button
|
||||
critical={!history}
|
||||
icon={history ? <FaUndo /> : <BiTrash />}
|
||||
recommended={!!history}
|
||||
tooltip={history ? 'Restore Note' : 'Clear Note'}
|
||||
tooltip={clearOrRestoreTooltip}
|
||||
onClick={() => (history ? restore() : clear())}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -73,7 +90,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
|
|||
<textarea
|
||||
className={styles.textarea}
|
||||
dir="auto"
|
||||
placeholder="What is on your mind?"
|
||||
placeholder={t('modals.notepad.placeholder')}
|
||||
ref={textareaRef}
|
||||
spellCheck={false}
|
||||
value={note}
|
||||
|
|
@ -82,8 +99,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
|
|||
/>
|
||||
|
||||
<p className={styles.counter}>
|
||||
{characters} character{characters !== 1 && 's'} • {words} word
|
||||
{words !== 1 && 's'}
|
||||
{t('modals.notepad.counter-stats', counterOptions)}
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index';
|
||||
import { IoMdSettings } from 'react-icons/io/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Button } from '../generics/button';
|
||||
import { Timer } from './timer';
|
||||
|
|
@ -12,7 +12,6 @@ import { useLocalStorage } from '@/hooks/use-local-storage';
|
|||
import { useSoundEffect } from '@/hooks/use-sound-effect';
|
||||
import { usePomodoroStore } from '@/stores/pomodoro';
|
||||
import { useCloseListener } from '@/hooks/use-close-listener';
|
||||
|
||||
import styles from './pomodoro.module.css';
|
||||
|
||||
interface PomodoroProps {
|
||||
|
|
@ -22,6 +21,7 @@ interface PomodoroProps {
|
|||
}
|
||||
|
||||
export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
||||
const { t } = useTranslation();
|
||||
const [showSetting, setShowSetting] = useState(false);
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState('pomodoro');
|
||||
|
|
@ -56,11 +56,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
|||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{ id: 'pomodoro', label: 'Pomodoro' },
|
||||
{ id: 'short', label: 'Break' },
|
||||
{ id: 'long', label: 'Long Break' },
|
||||
{ id: 'pomodoro', label: t('modals.pomodoro.tabs.pomodoro') },
|
||||
{ id: 'short', label: t('modals.pomodoro.tabs.short-break') },
|
||||
{ id: 'long', label: t('modals.pomodoro.tabs.long-break') },
|
||||
],
|
||||
[],
|
||||
[t],
|
||||
);
|
||||
|
||||
useCloseListener(() => setShowSetting(false));
|
||||
|
|
@ -123,12 +123,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
|||
<>
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Pomodoro Timer</h2>
|
||||
|
||||
<h2 className={styles.title}>{t('modals.pomodoro.title')}</h2>
|
||||
<div className={styles.button}>
|
||||
<Button
|
||||
icon={<IoMdSettings />}
|
||||
tooltip="Change Times"
|
||||
tooltip={t('modals.pomodoro.settings-tooltip')}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setShowSetting(true);
|
||||
|
|
@ -142,19 +141,21 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
|||
|
||||
<div className={styles.control}>
|
||||
<p className={styles.completed}>
|
||||
{completions[selectedTab] || 0} completed
|
||||
{t('modals.pomodoro.completed', {
|
||||
count: completions[selectedTab] || 0,
|
||||
})}
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
icon={<FaUndo />}
|
||||
smallIcon
|
||||
tooltip="Restart"
|
||||
tooltip={t('common.restart')}
|
||||
onClick={restart}
|
||||
/>
|
||||
<Button
|
||||
icon={running ? <FaPause /> : <FaPlay />}
|
||||
smallIcon
|
||||
tooltip={running ? 'Pause' : 'Start'}
|
||||
tooltip={running ? t('common.pause') : t('common.start')}
|
||||
onClick={toggleRunning}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import styles from './setting.module.css';
|
||||
|
|
@ -12,6 +12,7 @@ interface SettingProps {
|
|||
}
|
||||
|
||||
export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
||||
const { t } = useTranslation();
|
||||
const [values, setValues] = useState<Record<string, number | string>>(times);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -46,34 +47,34 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
|||
|
||||
return (
|
||||
<Modal lockBody={false} show={show} onClose={onClose}>
|
||||
<h2 className={styles.title}>Change Times</h2>
|
||||
<h2 className={styles.title}>{t('modals.pomodoro.settings.title')}</h2>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSubmit}>
|
||||
<Field
|
||||
id="pomodoro"
|
||||
label="Pomodoro"
|
||||
labelKey="modals.pomodoro.settings.pomodoro-label"
|
||||
value={values.pomodoro}
|
||||
onChange={handleChange('pomodoro')}
|
||||
/>
|
||||
<Field
|
||||
id="short"
|
||||
label="Short Break"
|
||||
labelKey="modals.pomodoro.settings.short-break-label"
|
||||
value={values.short}
|
||||
onChange={handleChange('short')}
|
||||
/>
|
||||
<Field
|
||||
id="long"
|
||||
label="Long Break"
|
||||
labelKey="modals.pomodoro.settings.long-break-label"
|
||||
value={values.long}
|
||||
onChange={handleChange('long')}
|
||||
/>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<button type="button" onClick={handleCancel}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button className={styles.primary} type="submit">
|
||||
Save
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -83,19 +84,22 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
|||
|
||||
interface FieldProps {
|
||||
id: string;
|
||||
label: string;
|
||||
labelKey: string;
|
||||
onChange: (value: number | string) => void;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
function Field({ id, label, onChange, value }: FieldProps) {
|
||||
function Field({ id, labelKey, onChange, value }: FieldProps) {
|
||||
const { t } = useTranslation(); // 获取翻译函数
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={id}>
|
||||
{label} <span>(minutes)</span>
|
||||
{t(labelKey)}{' '}
|
||||
<span>({t('modals.pomodoro.settings.minutes-unit')})</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
id={id}
|
||||
max={120}
|
||||
min={1}
|
||||
required
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTodoStore } from '@/stores/todo';
|
||||
|
||||
import styles from './form.module.css';
|
||||
|
||||
export function Form() {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const addTodo = useTodoStore(state => state.addTodo);
|
||||
|
|
@ -22,12 +23,12 @@ export function Form() {
|
|||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.wrapper}>
|
||||
<input
|
||||
placeholder="I have to ..."
|
||||
placeholder={t('modals.todo.add-placeholder')}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
<button type="submit">Add</button>
|
||||
<button type="submit">{t('modals.todo.add-button')}</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Form } from './form';
|
||||
import { Todos } from './todos';
|
||||
|
|
@ -10,11 +11,13 @@ interface TodoProps {
|
|||
}
|
||||
|
||||
export function Todo({ onClose, show }: TodoProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Todo Checklist</h2>
|
||||
<p className={styles.desc}>Super simple todo list.</p>
|
||||
<h2 className={styles.title}>{t('modals.todo.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.todo.description')}</p>
|
||||
</header>
|
||||
|
||||
<Form />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { FaRegTrashAlt } from 'react-icons/fa/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Checkbox } from '@/components/checkbox';
|
||||
|
||||
import { useTodoStore } from '@/stores/todo';
|
||||
import { cn } from '@/helpers/styles';
|
||||
|
||||
import styles from './todo.module.css';
|
||||
import { Tooltip } from '@/components/tooltip';
|
||||
|
||||
interface TodoProps {
|
||||
done: boolean;
|
||||
|
|
@ -14,6 +15,7 @@ interface TodoProps {
|
|||
}
|
||||
|
||||
export function Todo({ done, id, todo }: TodoProps) {
|
||||
const { t } = useTranslation();
|
||||
const deleteTodo = useTodoStore(state => state.deleteTodo);
|
||||
const toggleTodo = useTodoStore(state => state.toggleTodo);
|
||||
const editTodo = useTodoStore(state => state.editTodo);
|
||||
|
|
@ -32,9 +34,17 @@ export function Todo({ done, id, todo }: TodoProps) {
|
|||
value={todo}
|
||||
onChange={e => editTodo(id, e.target.value)}
|
||||
/>
|
||||
<button className={styles.delete} onClick={handleDelete}>
|
||||
<FaRegTrashAlt />
|
||||
</button>
|
||||
<Tooltip content={t('common.delete')} showDelay={0}>
|
||||
<button
|
||||
className={styles.delete}
|
||||
aria-label={
|
||||
t('modals.todo.delete-button-aria-label') || `Delete todo: ${todo}`
|
||||
}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<FaRegTrashAlt />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Todo } from './todo';
|
||||
|
||||
import { useTodoStore } from '@/stores/todo';
|
||||
|
|
@ -5,13 +6,14 @@ import { useTodoStore } from '@/stores/todo';
|
|||
import styles from './todos.module.css';
|
||||
|
||||
export function Todos() {
|
||||
const { t } = useTranslation();
|
||||
const todos = useTodoStore(state => state.todos);
|
||||
const doneCount = useTodoStore(state => state.doneCount());
|
||||
|
||||
return (
|
||||
<div className={styles.todos}>
|
||||
<header>
|
||||
<p className={styles.label}>Your Todos</p>
|
||||
<p className={styles.label}>{t('modals.todo.your-todos-label')}</p>
|
||||
<div className={styles.divider} />
|
||||
<p className={styles.counter}>
|
||||
{doneCount} / {todos.length}
|
||||
|
|
@ -30,7 +32,7 @@ export function Todos() {
|
|||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className={styles.empty}>You don't have any todos.</p>
|
||||
<p className={styles.empty}>{t('modals.todo.empty')}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,11 +8,156 @@
|
|||
"en": "English",
|
||||
"zh": "简体中文"
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"untitled": "Untitled",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied!",
|
||||
"override": "Override",
|
||||
"done": "Done!",
|
||||
"reset": "Reset",
|
||||
"start": "Start",
|
||||
"pause": "Pause",
|
||||
"stop": "Stop",
|
||||
"back": "Back",
|
||||
"restart": "Restart",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"plural-suffix": "s"
|
||||
},
|
||||
"toolbar": {
|
||||
"menu-aria-label": "Menu",
|
||||
"global-volume-label": "Global Volume",
|
||||
"items": {
|
||||
"presets": "Your Presets",
|
||||
"share": "Share Sounds",
|
||||
"shuffle": "Shuffle Sounds",
|
||||
"sleep-timer": "Sleep Timer",
|
||||
"countdown": "Countdown Timer",
|
||||
"pomodoro": "Pomodoro",
|
||||
"notepad": "Notepad",
|
||||
"todo": "Todo Checklist",
|
||||
"breathing": "Breathing Exercise",
|
||||
"binaural": "Binaural Beats",
|
||||
"isochronic": "Isochronic Tones",
|
||||
"shortcuts": "Shortcuts",
|
||||
"buy-me-a-coffee": "Buy Me a Coffee",
|
||||
"source-code": "Source Code"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"reload": {
|
||||
"title": "New Content",
|
||||
"description": "New content available, click on reload button to update.",
|
||||
"reloadButton": "Reload"
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"your-presets-title": "Your Presets",
|
||||
"empty": "You don't have any presets yet.",
|
||||
"new-preset-title": "New Preset",
|
||||
"placeholder": "Preset's Name",
|
||||
"play-button-tooltip": "Play preset",
|
||||
"play-button-aria-label": "Play preset {{label}}",
|
||||
"delete-button-tooltip": "Delete preset",
|
||||
"delete-button-aria-label": "Delete preset {{label}}",
|
||||
"no-selected-warning": "To make a preset, first select some sounds."
|
||||
},
|
||||
"share-link": {
|
||||
"title": "Share your sound selection!",
|
||||
"description": "Copy and send the following link to the person you want to share your selection with.",
|
||||
"copy-button-aria-label": "Copy link"
|
||||
},
|
||||
"shared": {
|
||||
"title": "New sound mix detected!",
|
||||
"description": "Someone has shared the following mix with you. Would you want to override your current selection?",
|
||||
"snackbar-message": "Done! You can now play the new selection."
|
||||
},
|
||||
"sleep-timer": {
|
||||
"title": "Sleep Timer",
|
||||
"description": "Stop sounds after a certain amount of time.",
|
||||
"hours-label": "Hours",
|
||||
"minutes-label": "Minutes"
|
||||
},
|
||||
"countdown": {
|
||||
"title": "Countdown Timer",
|
||||
"description": "Super simple countdown timer.",
|
||||
"placeholder-hh": "HH",
|
||||
"placeholder-mm": "MM",
|
||||
"placeholder-ss": "SS"
|
||||
},
|
||||
"pomodoro": {
|
||||
"title": "Pomodoro Timer",
|
||||
"settings-tooltip": "Change Times",
|
||||
"completed": "{{count}} completed",
|
||||
"tabs": {
|
||||
"pomodoro": "Pomodoro",
|
||||
"short-break": "Break",
|
||||
"long-break": "Long Break"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Change Times",
|
||||
"pomodoro-label": "Pomodoro",
|
||||
"short-break-label": "Short Break",
|
||||
"long-break-label": "Long Break",
|
||||
"minutes-unit": "minutes"
|
||||
}
|
||||
},
|
||||
"notepad": {
|
||||
"title-label": "Your Note",
|
||||
"copy-tooltip": "Copy Note",
|
||||
"download-tooltip": "Download Note",
|
||||
"clear-tooltip": "Clear Note",
|
||||
"restore-tooltip": "Restore Note",
|
||||
"placeholder": "What is on your mind?",
|
||||
"counter-stats": "{{chars}} character{{chars_plural}} • {{words}} word{{words_plural}}"
|
||||
},
|
||||
"todo": {
|
||||
"title": "Todo Checklist",
|
||||
"description": "Super simple todo list.",
|
||||
"add-placeholder": "I have to ...",
|
||||
"add-button": "Add",
|
||||
"your-todos-label": "Your Todos",
|
||||
"empty": "You don't have any todos.",
|
||||
"delete-button-aria-label": "Delete todo"
|
||||
},
|
||||
"breathing": {
|
||||
"title": "Breathing Exercise",
|
||||
"phases": {
|
||||
"inhale": "Inhale",
|
||||
"exhale": "Exhale",
|
||||
"hold": "Hold"
|
||||
},
|
||||
"exercises": {
|
||||
"box": "Box Breathing",
|
||||
"resonant": "Resonant Breathing",
|
||||
"478": "4-7-8 Breathing"
|
||||
}
|
||||
},
|
||||
"generators": {
|
||||
"presets-label": "Presets:",
|
||||
"base-frequency-label": "Base Frequency (Hz):",
|
||||
"volume-label": "Volume:",
|
||||
"presets": {
|
||||
"delta": "Delta (Deep Sleep) 2 Hz",
|
||||
"theta": "Theta (Meditation) 5 Hz",
|
||||
"alpha": "Alpha (Relaxation) 10 Hz",
|
||||
"beta": "Beta (Focus) 20 Hz",
|
||||
"gamma": "Gamma (Cognition) 40 Hz",
|
||||
"custom": "Custom"
|
||||
}
|
||||
},
|
||||
"binaural": {
|
||||
"title": "Binaural Beat",
|
||||
"description": "Binaural beat generator.",
|
||||
"beat-frequency-label": "Beat Frequency (Hz):"
|
||||
},
|
||||
"isochronic": {
|
||||
"title": "Isochronic Tone",
|
||||
"description": "Isochronic tone generator.",
|
||||
"tone-frequency-label": "Tone Frequency (Hz):"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"labels": {
|
||||
"toggle-play": "Toggle Play",
|
||||
"unselect-all": "Unselect All Sounds"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,156 @@
|
|||
"en": "English",
|
||||
"zh": "简体中文"
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"untitled": "未命名",
|
||||
"copy": "复制",
|
||||
"copied": "已复制!",
|
||||
"override": "覆盖",
|
||||
"done": "完成!",
|
||||
"reset": "重置",
|
||||
"start": "开始",
|
||||
"pause": "暂停",
|
||||
"stop": "停止",
|
||||
"back": "返回",
|
||||
"restart": "重新开始",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"plural-suffix": ""
|
||||
},
|
||||
"toolbar": {
|
||||
"menu-aria-label": "菜单",
|
||||
"global-volume-label": "全局音量",
|
||||
"items": {
|
||||
"presets": "预设",
|
||||
"share": "分享声音",
|
||||
"shuffle": "随机播放",
|
||||
"sleep-timer": "睡眠定时器",
|
||||
"countdown": "倒计时器",
|
||||
"pomodoro": "番茄钟",
|
||||
"notepad": "记事本",
|
||||
"todo": "待办清单",
|
||||
"breathing": "呼吸练习",
|
||||
"binaural": "双耳节拍",
|
||||
"isochronic": "等时声频",
|
||||
"shortcuts": "快捷键",
|
||||
"buy-me-a-coffee": "请我喝咖啡",
|
||||
"source-code": "源代码"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"reload": {
|
||||
"title": "发现新内容",
|
||||
"description": "检测到可用新内容,点击“重新加载”按钮进行更新。",
|
||||
"reloadButton": "重新加载"
|
||||
"presets": {
|
||||
"title": "预设",
|
||||
"your-presets-title": "您的预设",
|
||||
"empty": "您还没有任何预设。",
|
||||
"new-preset-title": "新建预设",
|
||||
"placeholder": "预设名称",
|
||||
"play-button-tooltip": "播放预设",
|
||||
"play-button-aria-label": "播放预设 {{label}}",
|
||||
"delete-button-tooltip": "删除预设",
|
||||
"delete-button-aria-label": "删除预设 {{label}}",
|
||||
"no-selected-warning": "要创建预设,请先选择一些声音。"
|
||||
},
|
||||
"share-link": {
|
||||
"title": "分享您的声音组合!",
|
||||
"description": "复制下面的链接并发送给您想与之分享的人。",
|
||||
"copy-button-aria-label": "复制链接"
|
||||
},
|
||||
"shared": {
|
||||
"title": "检测到新的声音组合!",
|
||||
"description": "有人与您分享了以下声音组合。您想覆盖当前的选择吗?",
|
||||
"snackbar-message": "完成!您现在可以播放新的选择了。"
|
||||
},
|
||||
"sleep-timer": {
|
||||
"title": "睡眠定时器",
|
||||
"description": "在指定时间后停止播放声音。",
|
||||
"hours-label": "小时",
|
||||
"minutes-label": "分钟"
|
||||
},
|
||||
"countdown": {
|
||||
"title": "倒计时器",
|
||||
"description": "超级简单的倒计时器。",
|
||||
"placeholder-hh": "时",
|
||||
"placeholder-mm": "分",
|
||||
"placeholder-ss": "秒"
|
||||
},
|
||||
"pomodoro": {
|
||||
"title": "番茄钟",
|
||||
"settings-tooltip": "更改时间",
|
||||
"completed": "已完成 {{count}} 次",
|
||||
"tabs": {
|
||||
"pomodoro": "番茄钟",
|
||||
"short-break": "短休息",
|
||||
"long-break": "长休息"
|
||||
},
|
||||
"settings": {
|
||||
"title": "更改时间",
|
||||
"pomodoro-label": "番茄钟",
|
||||
"short-break-label": "短休息",
|
||||
"long-break-label": "长休息",
|
||||
"minutes-unit": "分钟"
|
||||
}
|
||||
},
|
||||
"notepad": {
|
||||
"title-label": "您的笔记",
|
||||
"copy-tooltip": "复制笔记",
|
||||
"download-tooltip": "下载笔记",
|
||||
"clear-tooltip": "清空笔记",
|
||||
"restore-tooltip": "恢复笔记",
|
||||
"placeholder": "您在想什么?",
|
||||
"counter-stats": "共 {{chars}} 个字符"
|
||||
},
|
||||
"todo": {
|
||||
"title": "待办清单",
|
||||
"description": "超级简单的待办事项列表。",
|
||||
"add-placeholder": "我需要做...",
|
||||
"add-button": "添加",
|
||||
"your-todos-label": "您的待办事项",
|
||||
"empty": "您还没有任何待办事项。",
|
||||
"delete-button-aria-label": "删除待办事项"
|
||||
},
|
||||
"breathing": {
|
||||
"title": "呼吸练习",
|
||||
"phases": {
|
||||
"inhale": "吸气",
|
||||
"exhale": "呼气",
|
||||
"hold": "屏息"
|
||||
},
|
||||
"exercises": {
|
||||
"box": "方形呼吸",
|
||||
"resonant": "共振呼吸",
|
||||
"478": "4-7-8 呼吸"
|
||||
}
|
||||
},
|
||||
"generators": {
|
||||
"presets-label": "预设:",
|
||||
"base-frequency-label": "基础频率 (Hz):",
|
||||
"volume-label": "音量:",
|
||||
"presets": {
|
||||
"delta": "Delta (深度睡眠) 2 Hz",
|
||||
"theta": "Theta (冥想) 5 Hz",
|
||||
"alpha": "Alpha (放松) 10 Hz",
|
||||
"beta": "Beta (专注) 20 Hz",
|
||||
"gamma": "Gamma (认知) 40 Hz",
|
||||
"custom": "自定义"
|
||||
}
|
||||
},
|
||||
"binaural": {
|
||||
"title": "双耳节拍",
|
||||
"description": "双耳节拍生成器。",
|
||||
"beat-frequency-label": "节拍频率 (Hz):"
|
||||
},
|
||||
"isochronic": {
|
||||
"title": "等时声频",
|
||||
"description": "等时声频生成器。",
|
||||
"tone-frequency-label": "声频频率 (Hz):"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "键盘快捷键",
|
||||
"labels": {
|
||||
"toggle-play": "播放/暂停",
|
||||
"unselect-all": "取消全选声音"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue