77 let completionScreen = null ;
88 let restartButton = null ;
99 let startOverButton = null ;
10+ let keyboardContainer = null ;
11+ let config = { keyboard : true } ;
1012
1113 // Character states: 'pending', 'correct', 'incorrect'
1214 const charStates = [ ] ;
1618 let totalErrors = 0 ;
1719 let totalInputs = 0 ;
1820
21+ // Keyboard state
22+ let keyboardEnabled = false ;
23+ let activeKeyElement = null ;
24+ let activeKeyTimeout = null ;
25+
1926 function setStatus ( msg ) {
2027 const status = document . getElementById ( 'status' ) ;
2128 if ( status ) {
2229 status . textContent = msg ;
2330 }
2431 }
2532
33+ // Load configuration
34+ async function loadConfig ( ) {
35+ try {
36+ const response = await fetch ( './config.json' ) ;
37+ if ( ! response . ok ) {
38+ console . warn ( 'Config file not found, using defaults' ) ;
39+ return ;
40+ }
41+ config = await response . json ( ) ;
42+ } catch ( error ) {
43+ console . warn ( 'Error loading config:' , error ) ;
44+ }
45+ }
46+
47+ // Keyboard layout definition
48+ const keyboardLayout = [
49+ [ '`' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '0' , '-' , '=' , 'backspace' ] ,
50+ [ 'tab' , 'q' , 'w' , 'e' , 'r' , 't' , 'y' , 'u' , 'i' , 'o' , 'p' , '[' , ']' , '\\' ] ,
51+ [ 'caps' , 'a' , 's' , 'd' , 'f' , 'g' , 'h' , 'j' , 'k' , 'l' , ';' , "'" , 'enter' ] ,
52+ [ 'shift' , 'z' , 'x' , 'c' , 'v' , 'b' , 'n' , 'm' , ',' , '.' , '/' , 'shift' ] ,
53+ [ 'space' ]
54+ ] ;
55+
56+ // Map special keys to display names
57+ const keyDisplayNames = {
58+ 'backspace' : '⌫' ,
59+ 'tab' : 'Tab' ,
60+ 'caps' : 'Caps' ,
61+ 'enter' : 'Enter' ,
62+ 'shift' : 'Shift' ,
63+ 'space' : 'Space'
64+ } ;
65+
66+ // Get key element by character
67+ function getKeyElement ( char ) {
68+ if ( ! keyboardContainer ) return null ;
69+
70+ // Normalize character
71+ const normalizedChar = char . toLowerCase ( ) ;
72+
73+ // Handle special keys
74+ if ( char === ' ' ) {
75+ return keyboardContainer . querySelector ( '[data-key="space"]' ) ;
76+ }
77+ if ( char === '\n' || char === '\r' ) {
78+ return keyboardContainer . querySelector ( '[data-key="enter"]' ) ;
79+ }
80+ if ( char === '\t' ) {
81+ return keyboardContainer . querySelector ( '[data-key="tab"]' ) ;
82+ }
83+
84+ // Find regular key
85+ return keyboardContainer . querySelector ( `[data-key="${ normalizedChar } "]` ) ;
86+ }
87+
88+ // Highlight a key on the keyboard
89+ function highlightKey ( char , isError = false ) {
90+ // Clear previous highlight
91+ if ( activeKeyElement ) {
92+ activeKeyElement . classList . remove ( 'active' , 'active-error' ) ;
93+ }
94+
95+ // Clear timeout if exists
96+ if ( activeKeyTimeout ) {
97+ clearTimeout ( activeKeyTimeout ) ;
98+ }
99+
100+ const keyElement = getKeyElement ( char ) ;
101+ if ( keyElement ) {
102+ activeKeyElement = keyElement ;
103+ if ( isError ) {
104+ keyElement . classList . add ( 'active-error' ) ;
105+ } else {
106+ keyElement . classList . add ( 'active' ) ;
107+ }
108+
109+ // Remove highlight after animation
110+ activeKeyTimeout = setTimeout ( ( ) => {
111+ if ( keyElement ) {
112+ keyElement . classList . remove ( 'active' , 'active-error' ) ;
113+ }
114+ activeKeyElement = null ;
115+ } , 200 ) ;
116+ }
117+ }
118+
119+ // Render the keyboard
120+ function renderKeyboard ( ) {
121+ if ( ! keyboardContainer ) return ;
122+
123+ const keyboard = document . createElement ( 'div' ) ;
124+ keyboard . className = 'keyboard' ;
125+
126+ keyboardLayout . forEach ( row => {
127+ const rowElement = document . createElement ( 'div' ) ;
128+ rowElement . className = 'keyboard-row' ;
129+
130+ row . forEach ( key => {
131+ const keyElement = document . createElement ( 'div' ) ;
132+ keyElement . className = 'keyboard-key' ;
133+ keyElement . setAttribute ( 'data-key' , key . toLowerCase ( ) ) ;
134+
135+ // Add special class for certain keys
136+ if ( key === 'space' || key === 'enter' || key === 'shift' ||
137+ key === 'backspace' || key === 'tab' || key === 'caps' ) {
138+ keyElement . classList . add ( key ) ;
139+ }
140+
141+ // Set display text
142+ if ( keyDisplayNames [ key ] ) {
143+ keyElement . textContent = keyDisplayNames [ key ] ;
144+ } else {
145+ keyElement . textContent = key . toUpperCase ( ) ;
146+ }
147+
148+ rowElement . appendChild ( keyElement ) ;
149+ } ) ;
150+
151+ keyboard . appendChild ( rowElement ) ;
152+ } ) ;
153+
154+ keyboardContainer . innerHTML = '' ;
155+ keyboardContainer . appendChild ( keyboard ) ;
156+ }
157+
158+ // Initialize keyboard
159+ function initializeKeyboard ( ) {
160+ keyboardContainer = document . getElementById ( 'keyboard-container' ) ;
161+ if ( ! keyboardContainer ) return ;
162+
163+ keyboardEnabled = config . keyboard === true ;
164+
165+ if ( keyboardEnabled ) {
166+ renderKeyboard ( ) ;
167+ keyboardContainer . classList . add ( 'visible' ) ;
168+ } else {
169+ keyboardContainer . classList . remove ( 'visible' ) ;
170+ }
171+ }
172+
26173 async function loadText ( ) {
27174 try {
28175 setStatus ( 'Loading...' ) ;
159306
160307 totalInputs ++ ; // Track total inputs
161308
162- if ( typedChar === expectedChar ) {
163- charStates [ charIndex ] = 'correct' ;
164- } else {
309+ const isError = typedChar !== expectedChar ;
310+ if ( isError ) {
165311 charStates [ charIndex ] = 'incorrect' ;
166312 totalErrors ++ ; // Track total errors (even if later fixed)
313+ } else {
314+ charStates [ charIndex ] = 'correct' ;
315+ }
316+
317+ // Highlight keyboard key
318+ if ( keyboardEnabled ) {
319+ highlightKey ( typedChar , isError ) ;
167320 }
168321 }
169322 typedText = input ;
177330 charStates [ i ] = 'pending' ;
178331 }
179332 }
333+
334+ // Highlight backspace key
335+ if ( keyboardEnabled ) {
336+ highlightKey ( 'backspace' , false ) ;
337+ }
180338 }
181339
182340 renderText ( ) ;
188346 e . preventDefault ( ) ;
189347 }
190348
349+ // Highlight special keys that might not trigger input event
350+ if ( keyboardEnabled ) {
351+ if ( e . key === 'Enter' || e . key === 'Return' ) {
352+ highlightKey ( '\n' , false ) ;
353+ } else if ( e . key === 'Tab' ) {
354+ highlightKey ( '\t' , false ) ;
355+ e . preventDefault ( ) ; // Prevent tab from moving focus
356+ }
357+ }
358+
191359 // Allow all other keys to work normally
192360 // The input handler will process the changes
193361 }
206374 totalErrors = 0 ;
207375 totalInputs = 0 ;
208376
377+ // Clear keyboard highlights
378+ if ( activeKeyElement ) {
379+ activeKeyElement . classList . remove ( 'active' , 'active-error' ) ;
380+ activeKeyElement = null ;
381+ }
382+ if ( activeKeyTimeout ) {
383+ clearTimeout ( activeKeyTimeout ) ;
384+ activeKeyTimeout = null ;
385+ }
386+
209387 // Show typing container and hide completion screen
210388 const typingTextContainer = document . querySelector ( '.typing-text-container' ) ;
211389 if ( typingTextContainer ) {
@@ -350,7 +528,10 @@ Generated: ${new Date().toLocaleString()}
350528 }
351529 }
352530
353- function initialize ( ) {
531+ async function initialize ( ) {
532+ // Load config first
533+ await loadConfig ( ) ;
534+
354535 textContainer = document . getElementById ( 'typing-text' ) ;
355536 hiddenInput = document . getElementById ( 'hidden-input' ) ;
356537 completionScreen = document . getElementById ( 'completion-screen' ) ;
@@ -362,6 +543,9 @@ Generated: ${new Date().toLocaleString()}
362543 return ;
363544 }
364545
546+ // Initialize keyboard
547+ initializeKeyboard ( ) ;
548+
365549 // Set up event listeners
366550 hiddenInput . addEventListener ( 'input' , handleInput ) ;
367551 hiddenInput . addEventListener ( 'keydown' , handleKeyDown ) ;
0 commit comments