CodeTyper Use Case: Code Breakdown - CodeTyper.tsx
This document provides a detailed breakdown of the CodeTyper.tsx component, which is the most complex and important component in the application.
State Management
The CodeTyper component uses a number of state variables to manage its behavior. Here is a breakdown of each one, along with a code snippet that shows how it is initialized:
-
selectedLanguage: The programming language that is currently selected by the user.const [selectedLanguage, setSelectedLanguage] = useState('javascript'); -
customCode: The code that the user has provided to practice with.const [customCode, setCustomCode] = useState(''); -
selectedTab: The tab that is currently selected by the user (either "Sample Code" or "Custom Code").const [selectedTab, setSelectedTab] = useState(0); -
selectedLLM: The large language model that is currently selected by the user.const [selectedLLM, setSelectedLLM] = useState('gemini'); -
prompt: The prompt that the user has provided to generate code with.const [prompt, setPrompt] = useState(''); -
isGenerating: A boolean that indicates whether the application is currently generating code.const [isGenerating, setIsGenerating] = useState(false); -
geminiApiKey: The user's Gemini API key.const [geminiApiKey, setGeminiApiKey] = useState(''); -
typingCode: The code that the user is currently typing.const [typingCode, setTypingCode] = useState(() => getRandomCode(selectedLanguage)); -
wpm: The user's words per minute.const [wpm, setWpm] = useState(0); -
accuracy: The user's typing accuracy.const [accuracy, setAccuracy] = useState(100); -
errors: The number of errors that the user has made.const [errors, setErrors] = useState(0); -
typingStarted: A boolean that indicates whether the user has started typing.const [typingStarted, setTypingStarted] = useState(false); -
typedChars: The characters that the user has typed so far.const [typedChars, setTypedChars] = useState(''); -
currentCharIndex: The index of the character that the user is currently typing.const [currentCharIndex, setCurrentCharIndex] = useState(0); -
cursorPosition: The position of the cursor in the code editor.const [cursorPosition, setCursorPosition] = useState<Position>({ line: 0, column: 0 }); -
startTime: The time at which the user started typing.const [startTime, setStartTime] = useState<number | null>(null); -
typingErrors: An array of objects that contains information about the errors that the user has made.const [typingErrors, setTypingErrors] = useState<TypingErrorInfo[]>([]);
Core Functions
The CodeTyper component has a number of core functions that are responsible for its behavior. Here is a breakdown of each one, along with a code snippet that shows how it is implemented:
-
handleKeyDown: This function is called every time the user presses a key. It is responsible for handling all of the user's input, including special keys likeTab,Enter, andBackspace.const handleKeyDown = (e: React.KeyboardEvent) => {
if (!typingStarted) return;
// Get expected character
const expectedChar = typingCode.charAt(currentCharIndex);
// Special key handling
if (e.key === 'Tab') {
e.preventDefault();
handleTabKey();
return;
}
if (e.key === 'Enter') {
e.preventDefault();
handleEnterKey();
return;
}
if (e.key === 'Backspace') {
e.preventDefault();
handleBackspaceKey();
return;
}
// Regular character handling
if (e.key.length === 1) {
e.preventDefault();
if (e.key === expectedChar) {
// Correct key
setTypedChars(prev => prev + e.key);
setCurrentCharIndex(prev => prev + 1);
// Remove any errors at this position if they exist (from backspace and retry)
setTypingErrors(prev => prev.filter(err => err.index !== currentCharIndex));
} else {
// Wrong key
setErrors(prev => prev + 1);
// Add to typing errors
setTypingErrors(prev => [
...prev,
{
index: currentCharIndex,
expected: expectedChar,
actual: e.key
}
]);
setTypedChars(prev => prev + e.key);
setCurrentCharIndex(prev => prev + 1);
// Recalculate accuracy
const totalChars = currentCharIndex + 1;
const totalErrors = errors + 1;
setAccuracy(Math.max(0, Math.round(100 - (totalErrors / totalChars) * 100)));
}
// Check if typing is completed
if (currentCharIndex + 1 >= typingCode.length) {
// Typing completed
setTypingStarted(false);
// Final WPM calculation
if (startTime) {
const now = Date.now();
const elapsedMinutes = (now - startTime) / 60000;
const wordCount = (typedChars.length + 1) / 5; // +1 for the last char
setWpm(Math.round(wordCount / elapsedMinutes));
}
}
}
}; -
handleEnterKey: This function is called when the user presses theEnterkey. It is responsible for adding a newline character to the user's typed text and then inserting the same indentation as the previous line.const handleEnterKey = () => {
const expectedChar = typingCode.charAt(currentCharIndex);
if (expectedChar === '\n') {
// Get the indentation from the current line BEFORE we move to the next.
const lines = typedChars.split('\n');
const currentLine = lines[lines.length - 1] || '';
const indentationMatch = currentLine.match(/^(\s*)/);
const indentation = indentationMatch ? indentationMatch[1] : '';
// Add the newline and the indentation from the previous line.
const textToInsert = '\n' + indentation;
setTypedChars(prev => prev + textToInsert);
// We need to advance the character index, but only by the number of characters
// that actually match the typingCode. This handles cases where the auto-indent
// is incorrect (e.g., for a closing brace).
let matchingChars = 0;
for (let i = 0; i < textToInsert.length; i++) {
if (textToInsert[i] === typingCode.charAt(currentCharIndex + i)) {
matchingChars++;
} else {
// Stop counting when a mismatch occurs.
break;
}
}
// The user is still responsible for typing the whole line, but we can advance
// past the auto-inserted newline and any correct indentation.
setCurrentCharIndex(prev => prev + matchingChars);
} else {
// Enter wasn't expected, count as an error.
setErrors(prev => prev + 1);
}
}; -
handleBackspaceKey: This function is called when the user presses theBackspacekey. It is responsible for deleting characters from the user's typed text. It also includes a "smart backspace" feature that allows the user to delete entire blocks of indentation at once.const handleBackspaceKey = () => {
if (currentCharIndex > 0) {
// Handle backspace - check if we're at the beginning of indentation
const pos = cursorPosition;
const lines = typingCode.split('\n');
const currentLine = lines[pos.line] || '';
const lineStart = currentLine.match(/^(\s+)/);
// If at the start of a line with indentation, delete a full tab/indentation block
if (pos.column > 0 && pos.column === lineStart?.[1].length) {
// How many spaces to delete (2 or 4 based on indentation)
const spacesToDelete = (lineStart[1].length % 4 === 0) ?
Math.min(4, pos.column) :
Math.min(2, pos.column);
setTypedChars(prev => prev.slice(0, -spacesToDelete));
setCurrentCharIndex(prev => prev - spacesToDelete);
} else {
// Normal single character backspace
setTypedChars(prev => prev.slice(0, -1));
setCurrentCharIndex(prev => prev - 1);
}
}
}; -
resetTyping: This function is called when the user clicks the "Restart" button. It is responsible for resetting the application to its initial state.const resetTyping = useCallback((newCode?: string) => {
setTypingCode(newCode ?? getRandomCode(selectedLanguage));
setTypingStarted(false);
setTypedChars('');
setCurrentCharIndex(0);
setCursorPosition({ line: 0, column: 0 });
setWpm(0);
setAccuracy(100);
setErrors(0);
setStartTime(null);
setTypingErrors([]);
}, [selectedLanguage, getRandomCode]);
Rendering Logic
The CodeTyper component uses the renderTypingArea function to render the code editor. This function uses the highlight.js library to provide syntax highlighting for the code. It also includes logic to display the user's typed text, the cursor, and any errors that the user has made.
const renderTypingArea = () => {
if (!typingStarted) {
// Show regular highlighted code when not typing in gray color
return (
<pre className="font-mono text-sm whitespace-pre-wrap">
<code className={`language-${selectedLanguage} text-gray-400 dark:text-gray-500`}>
{typingCode}
</code>
</pre>
);
}
const highlightedCode = getHighlightedCode(typingCode, selectedLanguage);
const lines = typingCode.split('\n');
let charIndex = 0;
let highlightedCharIndex = 0;
// Function to parse highlighted HTML and create spans with classes
const parseHighlightedHtml = (htmlString: string) => {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
const spans: { char: string; className: string; index: number }[] = [];
const traverse = (node: Node) => {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent || '';
for (const char of text) {
spans.push({ char, className: '', index: highlightedCharIndex });
highlightedCharIndex++;
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement;
const className = element.className;
for (const child of Array.from(element.childNodes)) {
if (child.nodeType === Node.TEXT_NODE) {
const text = child.textContent || '';
for (const char of text) {
spans.push({ char, className, index: highlightedCharIndex });
highlightedCharIndex++;
}
} else if (child.nodeType === Node.ELEMENT_NODE) {
traverse(child);
}
}
}
};
traverse(doc.body);
return spans;
};
const highlightedSpans = parseHighlightedHtml(highlightedCode);
return (
<div className="font-mono text-sm whitespace-pre">
{lines.map((line, lineIdx) => {
const lineContent = [];
const lineStartCharIndex = charIndex;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const currentIndex = lineStartCharIndex + i;
const isTyped = currentIndex < currentCharIndex;
const isCurrent = currentIndex === currentCharIndex;
const isError = hasError(currentIndex);
const highlightedSpan = highlightedSpans.find(s => s.index === currentIndex);
const syntaxClass = highlightedSpan ? highlightedSpan.className : '';
if (isTyped) {
lineContent.push(
<span
key={`char-${currentIndex}-${char.charCodeAt(0)}`}
className={`${isError ? 'text-red-500 underline decoration-red-500 decoration-wavy' : syntaxClass}`}
>
{char}
</span>
);
} else if (isCurrent) {
lineContent.push(
<span key={`cursor-${currentIndex}`} ref={cursorRef} className="bg-primary text-primary-foreground">
{char}
</span>
);
} else {
lineContent.push(
<span key={`untyped-${currentIndex}`} className="text-gray-400 dark:text-gray-500">
{char}
</span>
);
}
}
// Add cursor at end of line if needed
if (cursorPosition.line === lineIdx && cursorPosition.column === line.length) {
if (currentCharIndex === charIndex) {
// If the next character is a newline, show a special cursor
lineContent.push(
<span key={`cursor-eol-${charIndex}`} className="bg-primary text-primary-foreground">
⏎
</span>
);
}
}
// Add newline character to the index count
if (lineIdx < lines.length - 1) {
charIndex += line.length + 1; // +1 for the newline character
} else {
charIndex += line.length;
}
return (
<div key={`line-${lineIdx}-${line.substring(0, Math.min(line.length, 5))}`} className="flex">
{lineContent.length > 0 ? lineContent : <span> </span>}
</div>
);
})}
</div>
);
};