Score Logging Implementation Guide
Overview
This document details the implementation of a comprehensive score logging system for the Code Typer application. The system tracks user typing performance, stores data locally using IndexedDB, and provides rich analytics with pagination and filtering capabilities.
Table of Contents
- Architecture Overview
- Implementation Details
- Problems Encountered & Solutions
- Technical Decisions
- File Structure
- API Reference
- Future Improvements
Architecture Overview
High-Level Architecture
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ CodeTyper │───▶│ SessionCompletion│───▶│ ScoreDatabase │
│ Component │ │ Modal │ │ (IndexedDB) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
└─────────────▶│ ScoreDisplay │◀─────────────┘
│ Component │
└──────────────────┘
Data Flow
- Session Tracking: CodeTyper monitors typing progress and calculates metrics
- Session Completion: When typing finishes, session data is prepared
- Modal Display: SessionCompletionModal shows results and saves to database
- Data Storage: ScoreDatabase handles IndexedDB operations
- Data Retrieval: ScoreDisplay fetches and presents historical data
Implementation Details
1. Database Layer (src/utils/scoreDatabase.ts)
Why IndexedDB?
The application is hosted on a domain with multiple apps, making IndexedDB the optimal choice:
- Isolation: Each app gets its own database namespace
- Storage Capacity: 50MB+ vs localStorage's 5-10MB limit
- Performance: Asynchronous operations don't block UI
- Structured Data: Better suited for complex queries and indexing
Database Schema
interface TypingSession {
id: string; // Unique identifier
timestamp: Date; // When the session occurred
language: string; // Programming language
codeSnippet: string; // The actual code typed
codeName?: string; // Name of sample code (if applicable)
wpm: number; // Words per minute
accuracy: number; // Percentage accuracy
errors: number; // Total error count
duration: number; // Session duration in seconds
totalCharacters: number; // Total characters in the code
source: 'sample' | 'custom' | 'ai-generated'; // Code source type
}
Key Database Operations
class ScoreDatabase {
// Initialize database with proper indexes
async init(): Promise<void>
// Save a new typing session
async saveSession(session: Omit<TypingSession, 'id'>): Promise<string>
// Get paginated sessions (most recent first)
async getAllSessions(limit?: number, offset?: number): Promise<TypingSession[]>
// Get daily top scores for progress tracking
async getDailyTopScores(): Promise<DailyTopScore[]>
// Get personal best records
async getPersonalBests(): Promise<PersonalBests>
}
2. Session Completion Modal (src/components/SessionCompletionModal.tsx)
Purpose
- Provide immediate feedback after session completion
- Show personal records and achievements
- Handle automatic session saving
- Encourage continued practice
Key Features
// New record detection
const [isNewRecord, setIsNewRecord] = useState({
wpm: false,
accuracy: false,
errors: false
});
// Performance-based encouragement messages
const getPerformanceMessage = () => {
if (wpm >= 80 && accuracy >= 95) return '🔥 Exceptional! You\'re a coding speed demon!';
if (wpm >= 60 && accuracy >= 90) return '⭐ Excellent work! Great speed and accuracy!';
// ... more conditions
};
Session Data Preparation
The modal receives comprehensive session data:
interface SessionData {
wpm: number;
accuracy: number;
errors: number;
duration: number;
language: string;
codeSnippet: string;
codeName?: string;
source: 'sample' | 'custom' | 'ai-generated';
totalCharacters: number;
}
3. Score Display Component (src/components/ScoreDisplay.tsx)
View Modes
All Sessions Mode
- Shows every typing session chronologically
- Pagination with configurable items per page (10, 25, 50, 100)
- Most recent sessions first
Daily Top Scores Mode
- Aggregates best performance per day per language
- Helps users track daily progress
- Shows improvement trends over time
Pagination Implementation
// Calculate pagination
const totalPages = viewMode === 'all'
? Math.ceil(totalSessions / itemsPerPage)
: Math.ceil(dailyTopScores.length / itemsPerPage);
// Handle page changes
const handleItemsPerPageChange = (items: ItemsPerPage) => {
setItemsPerPage(items);
setCurrentPage(1); // Reset to first page
};
Personal Bests Dashboard
const [personalBests, setPersonalBests] = useState<{
bestWpm: TypingSession | null;
bestAccuracy: TypingSession | null;
leastErrors: TypingSession | null;
}>({ bestWpm: null, bestAccuracy: null, leastErrors: null });
4. Integration with CodeTyper (src/components/CodeTyper.tsx)
Session Completion Detection
// Check if typing is completed
if (currentCharIndex + 1 >= typingCode.length) {
setTypingStarted(false);
if (startTime) {
const now = Date.now();
const elapsedSeconds = (now - startTime) / 1000;
const finalWpm = Math.round(wordCount / elapsedMinutes);
// Determine source and code name
let source: 'sample' | 'custom' | 'ai-generated' = 'custom';
let codeName: string | undefined;
if (selectedTab === 0) {
source = 'sample';
const langCodes = sampleCodes[selectedLanguage as keyof typeof sampleCodes];
const matchingCode = langCodes.find(code => code.code === typingCode);
codeName = matchingCode?.name;
} else {
source = prompt.trim() !== '' || isGenerating ? 'ai-generated' : 'custom';
}
// Prepare and show completion modal
setCompletedSessionData(sessionData);
setShowCompletionModal(true);
}
}
Smart Source Detection
The system intelligently determines the source of code being typed:
- Sample: Pre-defined code snippets from
sampleCodes.ts - Custom: User-written code in the custom tab
- AI-Generated: Code generated using the AI prompt feature
5. App Integration (src/App.tsx)
Scores Button Addition
<button
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background dark:bg-gray-800 shadow-sm hover:bg-accent hover:text-accent-foreground h-9 px-3"
type="button"
onClick={() => setShowScores(true)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 3v18h18"/>
<path d="m19 9-5 5-4-4-3 3"/>
</svg>
<span className="hidden sm:inline">Scores</span>
</button>
Problems Encountered & Solutions
1. Storage Conflicts on Shared Domain
Problem: Multiple apps on the same domain competing for localStorage space and potentially overwriting each other's data.
Solution:
- Chose IndexedDB over localStorage for complete isolation
- Each app gets its own database namespace
- Much larger storage capacity (50MB+ vs 5-10MB)
2. Session Completion Detection
Problem: Determining exactly when a typing session is complete and calculating final metrics.
Solution:
// Check completion in the keydown handler
if (currentCharIndex + 1 >= typingCode.length) {
// Session complete - calculate final metrics
const elapsedSeconds = (now - startTime) / 1000;
const finalWpm = Math.round(wordCount / elapsedMinutes);
// Trigger completion modal
setCompletedSessionData(sessionData);
setShowCompletionModal(true);
}
3. Source Type Detection
Problem: Automatically determining whether code came from samples, custom input, or AI generation.
Solution:
// Smart source detection logic
let source: 'sample' | 'custom' | 'ai-generated' = 'custom';
let codeName: string | undefined;
if (selectedTab === 0) {
// Sample code tab
source = 'sample';
const langCodes = sampleCodes[selectedLanguage as keyof typeof sampleCodes];
const matchingCode = langCodes.find(code => code.code === typingCode);
codeName = matchingCode?.name;
} else {
// Custom code tab - check if AI generated
source = prompt.trim() !== '' || isGenerating ? 'ai-generated' : 'custom';
}
4. Pagination Performance
Problem: Loading all sessions at once could be slow with large datasets.
Solution:
- Implemented proper IndexedDB pagination with offset/limit
- Used cursors for efficient data traversal
- Separate count query for total records
async getAllSessions(limit?: number, offset?: number): Promise<TypingSession[]> {
const request = index.openCursor(null, 'prev'); // Most recent first
let count = 0;
let skipped = 0;
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result;
if (cursor) {
if (offset && skipped < offset) {
skipped++;
cursor.continue();
return;
}
if (!limit || count < limit) {
sessions.push(cursor.value);
count++;
cursor.continue();
}
}
};
}
5. Daily Top Scores Calculation
Problem: Efficiently calculating the best score per day per language from all sessions.
Solution:
async getDailyTopScores(): Promise<DailyTopScore[]> {
const allSessions = await this.getAllSessions();
const dailyScores = new Map<string, TypingSession>();
// Group by date and language, keep best score
allSessions.forEach(session => {
const date = new Date(session.timestamp).toISOString().split('T')[0];
const key = `${date}_${session.language}`;
const existing = dailyScores.get(key);
if (!existing || this.compareScores(session, existing) > 0) {
dailyScores.set(key, session);
}
});
return Array.from(dailyScores.values())
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}
6. Personal Best Tracking
Problem: Efficiently determining if a new session beats existing personal records.
Solution:
// Multi-criteria comparison function
private compareScores(a: TypingSession, b: TypingSession): number {
// Primary: WPM (higher is better)
if (a.wpm !== b.wpm) return a.wpm - b.wpm;
// Secondary: Accuracy (higher is better)
if (a.accuracy !== b.accuracy) return a.accuracy - b.accuracy;
// Tertiary: Fewer errors (lower is better)
return b.errors - a.errors;
}
7. TypeScript Integration
Problem: Ensuring type safety across all components and database operations.
Solution:
- Defined comprehensive interfaces for all data structures
- Used proper TypeScript generics for database operations
- Implemented strict type checking for session data
8. Dark Mode Compatibility
Problem: Ensuring all new components work properly in both light and dark modes.
Solution:
- Used Tailwind's dark mode classes consistently
- Passed
darkModeprop through component hierarchy - Tested all UI states in both themes
Technical Decisions
1. IndexedDB over localStorage
- Reason: Domain isolation, larger capacity, better performance
- Trade-off: Slightly more complex API, but wrapped in simple interface
2. Modal-based Score Display
- Reason: Non-intrusive, focused user experience
- Trade-off: Could have been a separate page, but modal keeps context
3. Immediate Session Saving
- Reason: Prevents data loss, immediate feedback
- Trade-off: Could batch saves, but immediate is more reliable
4. Client-side Only Storage
- Reason: Simplicity, privacy, no backend required
- Trade-off: No cross-device sync, but fits project scope
5. Comprehensive Session Metadata
- Reason: Rich analytics possibilities, future-proofing
- Trade-off: Larger storage footprint, but negligible impact
File Structure
src/
├── components/
│ ├── CodeTyper.tsx # Modified: Added session completion
│ ├── ScoreDisplay.tsx # New: Score viewing with pagination
│ └── SessionCompletionModal.tsx # New: Post-session feedback
├── utils/
│ └── scoreDatabase.ts # New: IndexedDB wrapper
└── App.tsx # Modified: Added scores button
API Reference
ScoreDatabase Methods
// Initialize database
await scoreDB.init(): Promise<void>
// Save new session
await scoreDB.saveSession(session: Omit<TypingSession, 'id'>): Promise<string>
// Get paginated sessions
await scoreDB.getAllSessions(limit?: number, offset?: number): Promise<TypingSession[]>
// Get session count
await scoreDB.getTotalSessionCount(): Promise<number>
// Get daily top scores
await scoreDB.getDailyTopScores(): Promise<DailyTopScore[]>
// Get personal bests
await scoreDB.getPersonalBests(): Promise<PersonalBests>
// Clear all data
await scoreDB.clearAllData(): Promise<void>
Component Props
// SessionCompletionModal
interface SessionCompletionModalProps {
isOpen: boolean;
onClose: () => void;
sessionData: SessionData | null;
darkMode: boolean;
}
// ScoreDisplay
interface ScoreDisplayProps {
darkMode: boolean;
isOpen: boolean;
onClose: () => void;
}
Future Improvements
1. Data Export/Import
// Export user data
async exportData(): Promise<string> {
const sessions = await this.getAllSessions();
return JSON.stringify(sessions, null, 2);
}
// Import user data
async importData(jsonData: string): Promise<void> {
const sessions = JSON.parse(jsonData);
for (const session of sessions) {
await this.saveSession(session);
}
}
2. Advanced Analytics
- Weekly/monthly progress charts
- Language-specific improvement tracking
- Error pattern analysis
- Performance predictions
3. Achievement System
interface Achievement {
id: string;
name: string;
description: string;
condition: (sessions: TypingSession[]) => boolean;
icon: string;
unlockedAt?: Date;
}
4. Cloud Sync (Optional)
- Firebase/Supabase integration
- Cross-device synchronization
- Backup and restore functionality
5. Performance Optimizations
- Virtual scrolling for large datasets
- Background data processing
- Caching strategies for frequently accessed data
6. Enhanced Filtering
interface FilterOptions {
language?: string;
source?: 'sample' | 'custom' | 'ai-generated';
dateRange?: { start: Date; end: Date };
minWpm?: number;
minAccuracy?: number;
}
Conclusion
The score logging implementation successfully addresses the requirements while providing a robust, scalable foundation for future enhancements. The use of IndexedDB ensures no conflicts with other applications on the domain, while the comprehensive UI provides users with detailed insights into their typing progress.
The modular architecture makes it easy to extend functionality, and the TypeScript implementation ensures type safety throughout the application. The system is ready for production use and can handle thousands of typing sessions efficiently.