init
This commit is contained in:
commit
d42e47b15b
31 changed files with 3045 additions and 0 deletions
122
src/hooks/useTypingEngine.ts
Normal file
122
src/hooks/useTypingEngine.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
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 }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue