10afc2bbb0
- useMorseAudio: wraps MorseAudioEngine, exposes currentSignal for FlashArea - useProgress: fetches /api/progress + /api/mnemonics, recordAnswer with local stats - FlashArea: gold flash on signal, height varies dot/dash - ScoreBar: level name, score, streak display - PracticeCard: play button, answer input, correct/incorrect feedback with mnemonic - GamePage: full practice loop, level-up modal at streak%10, anti-repeat history - Fix: incorrect attempt recorded only once per challenge (no stat inflation on retries) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
import { useRef, useState, useCallback } from 'react'
|
|
import { MorseAudioEngine } from '../lib/audio'
|
|
import { DEFAULT_WPM } from '../lib/morse'
|
|
|
|
type SignalType = 'dot' | 'dash' | null
|
|
|
|
export function useMorseAudio() {
|
|
const engineRef = useRef<MorseAudioEngine | null>(null)
|
|
const [isPlaying, setIsPlaying] = useState(false)
|
|
const [currentSignal, setCurrentSignal] = useState<SignalType>(null)
|
|
|
|
function getEngine() {
|
|
if (!engineRef.current) {
|
|
const engine = new MorseAudioEngine()
|
|
engine.on('signal-start', ({ type }: { type: 'dot' | 'dash' }) => setCurrentSignal(type))
|
|
engine.on('signal-end', () => setCurrentSignal(null))
|
|
engine.on('play-end', () => { setIsPlaying(false); setCurrentSignal(null) })
|
|
engineRef.current = engine
|
|
}
|
|
return engineRef.current
|
|
}
|
|
|
|
const play = useCallback((text: string) => {
|
|
const engine = getEngine()
|
|
engine.stop()
|
|
setIsPlaying(true)
|
|
setCurrentSignal(null)
|
|
engine.play(text, DEFAULT_WPM)
|
|
}, [])
|
|
|
|
const stop = useCallback(() => {
|
|
engineRef.current?.stop()
|
|
setIsPlaying(false)
|
|
setCurrentSignal(null)
|
|
}, [])
|
|
|
|
return { play, stop, isPlaying, currentSignal }
|
|
}
|