122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
|
|
export type TypingEngineResult = {
|
|
currentIndex: number
|
|
errors: Set<number>
|
|
wpm: number
|
|
accuracy: number
|
|
isComplete: boolean
|
|
keyStats: Record<string, { hits: number; misses: number }>
|
|
handleKeyDown: (e: React.KeyboardEvent) => void
|
|
reset: (newText?: string) => void
|
|
}
|
|
|
|
export function useTypingEngine(
|
|
initialText: string,
|
|
onComplete?: (result: { wpm: number; accuracy: number; keyStats: Record<string, { hits: number; misses: number }> }) => void,
|
|
): TypingEngineResult {
|
|
const [text, setText] = useState(initialText)
|
|
const [currentIndex, setCurrentIndex] = useState(0)
|
|
const [errors, setErrors] = useState<Set<number>>(new Set())
|
|
const [totalKeystrokes, setTotalKeystrokes] = useState(0)
|
|
const [correctKeystrokes, setCorrectKeystrokes] = useState(0)
|
|
const [isComplete, setIsComplete] = useState(false)
|
|
const [keyStats, setKeyStats] = useState<Record<string, { hits: number; misses: number }>>({})
|
|
|
|
const startTimeRef = useRef<number | null>(null)
|
|
const [wpm, setWpm] = useState(0)
|
|
const onCompleteRef = useRef(onComplete)
|
|
onCompleteRef.current = onComplete
|
|
|
|
const accuracy = totalKeystrokes > 0 ? Math.round((correctKeystrokes / totalKeystrokes) * 100) : 100
|
|
|
|
// Update WPM periodically
|
|
useEffect(() => {
|
|
if (isComplete || currentIndex === 0) return
|
|
const interval = setInterval(() => {
|
|
if (startTimeRef.current) {
|
|
const minutes = (Date.now() - startTimeRef.current) / 60000
|
|
if (minutes > 0) {
|
|
setWpm(Math.round((currentIndex / 5) / minutes))
|
|
}
|
|
}
|
|
}, 500)
|
|
return () => clearInterval(interval)
|
|
}, [currentIndex, isComplete])
|
|
|
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
if (isComplete) return
|
|
// Ignore modifier keys, function keys, etc.
|
|
if (e.key.length > 1 && e.key !== 'Backspace') return
|
|
e.preventDefault()
|
|
|
|
if (e.key === 'Backspace') {
|
|
setCurrentIndex(prev => Math.max(0, prev - 1))
|
|
setErrors(prev => {
|
|
const next = new Set(prev)
|
|
next.delete(currentIndex - 1)
|
|
return next
|
|
})
|
|
return
|
|
}
|
|
|
|
if (!startTimeRef.current) {
|
|
startTimeRef.current = Date.now()
|
|
}
|
|
|
|
const expected = text[currentIndex]
|
|
const typed = e.key
|
|
const charKey = expected?.toLowerCase() ?? typed.toLowerCase()
|
|
|
|
setTotalKeystrokes(prev => prev + 1)
|
|
|
|
if (typed === expected) {
|
|
setCorrectKeystrokes(prev => prev + 1)
|
|
setKeyStats(prev => ({
|
|
...prev,
|
|
[charKey]: { hits: (prev[charKey]?.hits ?? 0) + 1, misses: prev[charKey]?.misses ?? 0 },
|
|
}))
|
|
const nextIndex = currentIndex + 1
|
|
setCurrentIndex(nextIndex)
|
|
|
|
if (nextIndex >= text.length) {
|
|
setIsComplete(true)
|
|
const minutes = (Date.now() - (startTimeRef.current ?? Date.now())) / 60000
|
|
const finalWpm = minutes > 0 ? Math.round((text.length / 5) / minutes) : 0
|
|
setWpm(finalWpm)
|
|
const finalAccuracy = (totalKeystrokes + 1) > 0
|
|
? Math.round(((correctKeystrokes + 1) / (totalKeystrokes + 1)) * 100)
|
|
: 100
|
|
// Build final keyStats including this last keystroke
|
|
setKeyStats(prev => {
|
|
const final = {
|
|
...prev,
|
|
[charKey]: { hits: (prev[charKey]?.hits ?? 0) + 1, misses: prev[charKey]?.misses ?? 0 },
|
|
}
|
|
onCompleteRef.current?.({ wpm: finalWpm, accuracy: finalAccuracy, keyStats: final })
|
|
return prev // Don't double-count, already set above
|
|
})
|
|
}
|
|
} else {
|
|
setErrors(prev => new Set(prev).add(currentIndex))
|
|
setKeyStats(prev => ({
|
|
...prev,
|
|
[charKey]: { hits: prev[charKey]?.hits ?? 0, misses: (prev[charKey]?.misses ?? 0) + 1 },
|
|
}))
|
|
}
|
|
}, [currentIndex, text, isComplete, totalKeystrokes, correctKeystrokes])
|
|
|
|
const reset = useCallback((newText?: string) => {
|
|
if (newText !== undefined) setText(newText)
|
|
setCurrentIndex(0)
|
|
setErrors(new Set())
|
|
setTotalKeystrokes(0)
|
|
setCorrectKeystrokes(0)
|
|
setIsComplete(false)
|
|
setKeyStats({})
|
|
setWpm(0)
|
|
startTimeRef.current = null
|
|
}, [])
|
|
|
|
return { currentIndex, errors, wpm, accuracy, isComplete, keyStats, handleKeyDown, reset }
|
|
}
|