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 | 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 | /**
* Letter Confirmation Hook
*
* Manages the letter confirmation state and logic for Learning mode.
* This hook handles:
* - Tracking confirmation progress
* - Keyboard event handling
* - Turn-based mode restrictions
* - Progress calculation
*/
'use client'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import type {
UseLetterConfirmationOptions,
UseLetterConfirmationReturn,
LetterStatus,
} from './types'
import {
getNthNonSpaceLetter,
normalizeToBaseLetter,
getLetterStatus as getLetterStatusUtil,
calculateProgress,
} from './letterUtils'
/**
* Hook for managing letter confirmation in Learning mode.
*
* This hook provides:
* - Keyboard event handling for letter input
* - Progress tracking and completion detection
* - Turn-based mode enforcement
* - Letter status calculation for display
*
* @example
* ```tsx
* const confirmation = useLetterConfirmation({
* regionName: 'France',
* requiredLetters: 3,
* confirmedCount: state.nameConfirmationProgress,
* isMyTurn: true,
* gameMode: 'cooperative',
* onConfirmLetter: (letter, index) => dispatch({ type: 'CONFIRM_LETTER', ... }),
* })
*
* if (confirmation.isComplete) {
* // Show "Now find it on the map"
* }
* ```
*/
export function useLetterConfirmation({
regionName,
requiredLetters,
confirmedCount,
isMyTurn,
gameMode,
onConfirmLetter,
onNotYourTurn,
}: UseLetterConfirmationOptions): UseLetterConfirmationReturn {
// Optimistic letter count ref - prevents race conditions when typing fast
const optimisticCountRef = useRef(confirmedCount)
// Sync optimistic count when server state updates
useEffect(() => {
optimisticCountRef.current = confirmedCount
}, [confirmedCount])
// Reset optimistic count when region changes
useEffect(() => {
optimisticCountRef.current = 0
}, [regionName])
// Calculate derived state
const isRequired = requiredLetters > 0
const isComplete = confirmedCount >= requiredLetters
// Get the next expected letter
const nextExpectedLetter = useMemo(() => {
if (!regionName || isComplete) return null
const letterInfo = getNthNonSpaceLetter(regionName, confirmedCount)
return letterInfo ? normalizeToBaseLetter(letterInfo.char) : null
}, [regionName, confirmedCount, isComplete])
// Calculate progress (0-1)
const progress = useMemo(
() => calculateProgress(confirmedCount, requiredLetters),
[confirmedCount, requiredLetters]
)
// Get letter status for display
const getLetterStatus = useCallback(
(nonSpaceIndex: number): LetterStatus => {
return getLetterStatusUtil(nonSpaceIndex, confirmedCount, requiredLetters, isComplete)
},
[confirmedCount, requiredLetters, isComplete]
)
// Handle keyboard input
useEffect(() => {
if (!isRequired || isComplete || !regionName) {
return
}
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if typing in an input or textarea
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return
}
// Only accept single character keys (letters only)
const pressedLetter = e.key.toLowerCase()
if (pressedLetter.length !== 1 || !/[a-z]/i.test(pressedLetter)) {
return
}
// In turn-based mode, only allow the current player to type
if (gameMode === 'turn-based' && !isMyTurn) {
onNotYourTurn?.()
return
}
// Use optimistic count to prevent race conditions when typing fast
const nextLetterIndex = optimisticCountRef.current
if (nextLetterIndex >= requiredLetters) {
return // Already confirmed all required letters
}
// Get the nth non-space letter (skipping spaces in the name)
const letterInfo = getNthNonSpaceLetter(regionName, nextLetterIndex)
if (!letterInfo) {
return // No more letters to confirm
}
// Normalize accented letters to base ASCII
const expectedLetter = normalizeToBaseLetter(letterInfo.char)
if (pressedLetter === expectedLetter) {
// Optimistically advance count before server responds
optimisticCountRef.current = nextLetterIndex + 1
// Dispatch to shared state
onConfirmLetter(pressedLetter, nextLetterIndex)
}
// Ignore wrong characters silently
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [
isRequired,
isComplete,
regionName,
requiredLetters,
isMyTurn,
gameMode,
onConfirmLetter,
onNotYourTurn,
])
return {
isComplete,
nextExpectedLetter,
progress,
isRequired,
getLetterStatus,
}
}
|