Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 41x 41x 41x 41x 41x 41x 58x 58x 58x 58x 58x 58x 58x 7x 7x 7x 7x 7x 4x 4x 4x 4x 3x 3x 3x 7x 3x 3x 3x 3x 2x 2x 2x 3x 58x 58x 58x 58x 58x 30x 29x 27x 27x 27x 27x 25x 25x 25x 27x 27x 27x 25x 25x 25x 25x 27x 10x 10x 7x 10x 10x 25x 25x 27x 30x 58x 58x 58x 58x 58x 3x 2x 58x 58x 58x 58x 2x 2x 58x 58x 58x 58x 27x 27x 58x 58x 58x 58x 53x 53x 53x 53x 53x 53x 58x 58x 58x 58x 26x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x 58x | 'use client'
import { useState, useCallback, useEffect, useRef } from 'react'
export interface UseKidNumberInputOptions {
/** The correct answer to validate against */
correctAnswer: number
/** Called when the correct answer is entered */
onCorrect: (answer: number, responseTimeMs: number) => void
/** Called when an incorrect answer is entered (optional) */
onIncorrect?: (answer: number, correctAnswer: number, responseTimeMs: number) => void
/** Whether input is disabled */
disabled?: boolean
/** Whether to listen for keyboard events globally (default: true) */
useGlobalKeyboard?: boolean
/** Maximum number of digits allowed (default: correctAnswer.toString().length) */
maxDigits?: number
/** Whether to clear input after correct answer (default: true, set false for form-like behavior) */
clearOnCorrect?: boolean
/** Whether to clear input after incorrect answer (default: true) */
clearOnIncorrect?: boolean
}
export interface KidNumberInputState {
/** Current input value as string */
value: string
/** Whether the input is currently disabled */
disabled: boolean
/** Time when input started (first digit entered) */
startTime: number | null
}
export interface KidNumberInputActions {
/** Add a digit to the input */
addDigit: (digit: string) => void
/** Remove the last digit */
backspace: () => void
/** Clear all input */
clear: () => void
/** Reset for a new question */
reset: () => void
}
/**
* Hook for kid-friendly number input with auto-validation.
*
* Automatically validates the input when the user has entered enough digits
* to potentially match the correct answer. If correct, calls onCorrect.
* If incorrect, calls onIncorrect and clears the input.
*
* @example
* ```tsx
* const { state, actions } = useKidNumberInput({
* correctAnswer: 7,
* onCorrect: (answer, time) => console.log('Correct!', answer, time),
* onIncorrect: (answer, correct, time) => console.log('Wrong', answer, correct),
* })
* ```
*/
export function useKidNumberInput({
correctAnswer,
onCorrect,
onIncorrect,
disabled = false,
useGlobalKeyboard = true,
maxDigits,
clearOnCorrect = true,
clearOnIncorrect = true,
}: UseKidNumberInputOptions) {
const [value, setValue] = useState('')
const [startTime, setStartTime] = useState<number | null>(null)
// Refs for stable callback references
const correctAnswerRef = useRef(correctAnswer)
const onCorrectRef = useRef(onCorrect)
const onIncorrectRef = useRef(onIncorrect)
const disabledRef = useRef(disabled)
const clearOnCorrectRef = useRef(clearOnCorrect)
const clearOnIncorrectRef = useRef(clearOnIncorrect)
// Update refs when props change
useEffect(() => {
correctAnswerRef.current = correctAnswer
onCorrectRef.current = onCorrect
onIncorrectRef.current = onIncorrect
disabledRef.current = disabled
clearOnCorrectRef.current = clearOnCorrect
clearOnIncorrectRef.current = clearOnIncorrect
}, [correctAnswer, onCorrect, onIncorrect, disabled, clearOnCorrect, clearOnIncorrect])
// Calculate max digits allowed
const effectiveMaxDigits = maxDigits ?? correctAnswer.toString().length
// Validate input when it reaches the expected length
const validateAndSubmit = useCallback((inputValue: string, inputStartTime: number | null) => {
const answer = parseInt(inputValue, 10)
const correct = correctAnswerRef.current
const responseTime = inputStartTime ? Date.now() - inputStartTime : 0
if (answer === correct) {
// Correct answer!
onCorrectRef.current(answer, responseTime)
// Optionally clear input (for games: clear immediately, for forms: keep displayed)
if (clearOnCorrectRef.current) {
setValue('')
setStartTime(null)
}
} else {
// Incorrect answer
onIncorrectRef.current?.(answer, correct, responseTime)
// Optionally clear input for retry
if (clearOnIncorrectRef.current) {
setValue('')
setStartTime(null)
}
}
}, [])
// Add a digit
const addDigit = useCallback(
(digit: string) => {
if (disabledRef.current) return
if (!/^[0-9]$/.test(digit)) return
setValue((prev) => {
// Don't add if already at max length
if (prev.length >= effectiveMaxDigits) return prev
const newValue = prev + digit
const newStartTime = startTime ?? Date.now()
// Update start time if this is the first digit
if (!startTime) {
setStartTime(newStartTime)
}
// Check if we should validate
if (newValue.length >= effectiveMaxDigits) {
// Use setTimeout to allow the state to update first
setTimeout(() => {
validateAndSubmit(newValue, newStartTime)
}, 50)
}
return newValue
})
},
[effectiveMaxDigits, startTime, validateAndSubmit]
)
// Remove last digit
const backspace = useCallback(() => {
if (disabledRef.current) return
setValue((prev) => prev.slice(0, -1))
}, [])
// Clear all input
const clear = useCallback(() => {
setValue('')
setStartTime(null)
}, [])
// Reset for new question
const reset = useCallback(() => {
setValue('')
setStartTime(null)
}, [])
// Global keyboard handler
useEffect(() => {
if (!useGlobalKeyboard) return
const handleKeyDown = (e: KeyboardEvent) => {
if (disabledRef.current) return
if (/^[0-9]$/.test(e.key)) {
e.preventDefault()
addDigit(e.key)
} else if (e.key === 'Backspace') {
e.preventDefault()
backspace()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [useGlobalKeyboard, addDigit, backspace])
// Reset when correctAnswer changes (new question)
useEffect(() => {
reset()
}, [correctAnswer, reset])
return {
state: {
value,
disabled,
startTime,
} as KidNumberInputState,
actions: {
addDigit,
backspace,
clear,
reset,
} as KidNumberInputActions,
}
}
|