A high-performance typing speed test application built with C++ compiled to WebAssembly and React, featuring real-time statistics, progress tracking, and global leaderboards. This project demonstrates modern web development techniques by combining native code performance with web application flexibility.
- Project Overview
- Key Features
- Technology Stack
- Architecture Overview
- Installation & Setup
- Project Structure
- How It Works
- Core Components
- User Interface
- Data Flow
- Performance Optimizations
- Development Workflow
- Building & Deployment
- Configuration
- Troubleshooting
- Learning Resources
- Contributing
- License
This is a full-stack typing speed test application that combines:
- C++ for high-performance business logic (compiled to WebAssembly)
- React for modern, reactive user interface
- Supabase for backend data persistence and real-time features
- WebAssembly as the bridge between C++ and JavaScript
Traditional Web Apps: JavaScript handles everything
- ✅ Easy to develop
- ❌ Slower for heavy computations
- ❌ Limited type safety
This Project: C++ for logic, JavaScript for UI
- ✅ Near-native performance for calculations
- ✅ Strong type safety in C++
- ✅ Modern, reactive UI with React
- ✅ Best of both worlds
The core innovation is using WebAssembly (WASM) to run C++ code directly in the browser. This allows us to:
- Write performance-critical code in C++ (typing calculations, text generation)
- Compile it to WebAssembly using Emscripten
- Call C++ functions from JavaScript/React
- Achieve near-native performance while maintaining web app flexibility
This architecture pattern is used by:
- Figma - Design tool with C++ rendering engine
- AutoCAD Web - CAD software running in browser
- Google Earth - 3D rendering with native code
- Photoshop Web - Image processing in browser
- Video editors - Real-time video processing
- Random Words: 25 random words (5-6 letters each) for speed practice
- Sentences: 3 complete sentences with simple vocabulary
- Mixed Case: 25 words with randomized capitalization for advanced practice
- WPM (Words Per Minute): Calculated using standard formula:
(correctChars / 5) / minutes - Accuracy: Percentage of correctly typed characters
- Timer: High-precision timer that starts on first keystroke
- Live Updates: Statistics update every 100ms during typing
- Auto-start Timer: Timer only begins when you type your first character
- Auto-complete: Test finishes automatically when all text is typed
- 60-Second Limit: Tests automatically end after 60 seconds
- Visual Feedback: Color-coded characters (green = correct, red = incorrect)
- Cursor Tracking: Visual cursor follows your typing position
Enter- Start test or restart after completionTab- Restart test during active sessionSpace- Start test (when not active)
- Restart Button: Restart current test (Tab key)
- Change Text Type Button: Return to home screen to select different text type
- Generator Selector: Choose between Random Words, Sentences, or Mixed Case
- Best WPM: Your highest words-per-minute score
- Best Accuracy: Your highest accuracy percentage
- Total Sessions: Count of all completed tests
- Leaderboard Position: Your rank among all users
- Line Charts: Interactive graphs showing WPM and Accuracy trends
- Day view: Hourly breakdown
- Week view: Daily averages
- Month view: Daily averages over 30 days
- Activity Heatmap: GitHub-style contribution graph
- Shows daily practice activity
- Color intensity indicates session count
- Year selector for historical data
- 53-week grid layout
- Complete History: All tests with date, time, WPM, accuracy, and duration
- Filtering Options:
- Today
- Yesterday
- Week (last 7 days)
- Month (last 30 days)
- Year (last 365 days)
- All Time
- Sortable Table: Click headers to sort by different metrics
- Each user has a unique profile URL:
/profile/YourUsername - Profiles can be shared with others
- View any user's progress and statistics
- Top 100 Users: Ranked by best score
- Score Comparison:
- Primary: WPM (higher is better)
- Secondary: Accuracy (higher is better)
- Tertiary: Time (lower is better)
- Medal System: 🥇 Gold, 🥈 Silver, 🥉 Bronze for top 3
- User Links: Click any username to view their profile
- Leaderboard updates automatically as new scores are submitted
- Best score per user (only your best score counts for ranking)
- Fair comparison system
- Desktop: Full-featured interface with all controls visible
- Tablet: Optimized layout with collapsible elements
- Mobile: Touch-friendly interface with simplified controls
- Breakpoints:
- Mobile: < 640px
- Tablet: 640px - 1024px
- Desktop: > 1024px
- Modern dark color scheme
- Easy on the eyes for extended use
- Custom color palette:
- Background:
#323437(primary),#2c2e31(secondary),#262729(tertiary) - Text:
#d1d0c5(primary),#646669(secondary),#72757e(tertiary) - Accent:
#e2b714(gold/yellow) - Correct:
#e2b714(green/gold) - Incorrect:
#ca4754(red)
- Background:
- Persistent Storage: Username saved in localStorage
- Auto-remember: Username persists across sessions
- Editable: Change username anytime from profile
- Validation: 50 character limit, trimmed whitespace
- React Router: Client-side routing for instant navigation
- Routes:
/- Typing test interface/leaderboard- Global rankings/profile- Your profile/profile/:username- Any user's profile
- Navigation Bar: Header with links to all pages
- Purpose: UI framework for building interactive interfaces
- Why React:
- Component-based architecture
- Efficient re-rendering with virtual DOM
- Large ecosystem and community
- Excellent developer experience
- Key Features Used:
- Hooks (useState, useEffect, useRef)
- Context API (implicit through props)
- Component composition
- Purpose: Client-side routing
- Why React Router:
- Declarative routing
- Browser history API integration
- Nested routes support
- URL parameter handling
- Purpose: Build tool and development server
- Why Vite:
- Lightning-fast HMR (Hot Module Replacement)
- Optimized production builds
- Native ES modules
- Plugin ecosystem
- Configuration: See
vite.config.js
- Purpose: Utility-first CSS framework
- Why Tailwind:
- Rapid UI development
- Consistent design system
- Small production bundle (unused styles removed)
- Customizable theme
- Configuration: See
tailwind.config.js
- Purpose: Charting library for progress graphs
- Why Recharts:
- React-native components
- Responsive by default
- Customizable styling
- Active development
- Purpose: Icon library
- Icons Used:
- Feather Icons (Fi*)
- Font Awesome 6 (Fa*)
- Purpose: Backend-as-a-Service (BaaS)
- Why Supabase:
- PostgreSQL database
- Real-time subscriptions
- Row Level Security (RLS)
- RESTful API
- Free tier available
- Features Used:
- Database storage (leaderboard table)
- Row Level Security policies
- REST API for queries
- Purpose: Compiles C++ to WebAssembly
- Version: Latest stable (check with
emcc --version) - Why Emscripten:
- Mature toolchain
- Excellent C++ support
- JavaScript interop
- Active development
- Output:
typing.js- JavaScript loader (~16KB)typing.wasm- WebAssembly binary (~152KB)
- Purpose: Binary format for running code in browsers
- Why WebAssembly:
- Near-native performance
- Language agnostic (C++, Rust, Go, etc.)
- Secure sandboxed execution
- Standardized by W3C
- Browser Support: All modern browsers (Chrome, Firefox, Safari, Edge)
- Purpose: JavaScript runtime
- Version: 18+ recommended
- Package Manager: npm (comes with Node.js)
- Purpose: Version control
- Repository: Track all changes
┌─────────────────────────────────────────────────────────────┐
│ Browser (Client) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ React UI Layer │◄────►│ WebAssembly Module │ │
│ │ (JavaScript) │Bridge│ (C++ Compiled) │ │
│ │ │ │ │ │
│ │ - TypingTest │ │ - TextGenerator │ │
│ │ - LeaderboardPage │ │ - TypingSession │ │
│ │ - ProfilePage │ │ - Timer │ │
│ │ - Components │ │ - Bindings │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │ │ │
│ │ │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ Supabase │ │
│ │ (Database)│ │
│ │ │ │
│ │ - Scores │ │
│ │ - Users │ │
│ │ - History │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────┘
App.jsx (Router)
│
├── TypingTest.jsx
│ ├── UsernameButton.jsx
│ ├── NameInputModal.jsx
│ └── wasmLoader.js (WASM bridge)
│
├── LeaderboardPage.jsx
│ └── UsernameButton.jsx
│
└── ProfilePage.jsx
└── UsernameButton.jsx
- User Input → React Component
- React Component → WASM Bridge (
wasmLoader.js) - WASM Bridge → WebAssembly Module (C++ code)
- C++ Calculation → Results returned
- Results → React State Update
- State Update → UI Re-render
- Score Save → Supabase Database
-
Node.js (v18 or higher)
- Download from nodejs.org
- Verify:
node --version - Includes npm (Node Package Manager)
-
Emscripten SDK
- Download from emscripten.org
- Or clone:
git clone https://github.com/emscripten-core/emsdk.git - Install:
./emsdk install latest && ./emsdk activate latest - Verify:
emcc --version
-
Git (optional, for version control)
- Download from git-scm.com
- VS Code or your preferred code editor
- Browser DevTools for debugging
# If using Git
git clone <repository-url>
cd cpp-react-wasm-typing-tutor
# Or download and extract ZIP file# Navigate to emsdk directory
cd ~/emsdk # or wherever you installed it
# Activate Emscripten
source ./emsdk_env.sh
# Verify installation
emcc --version
# Should output: emcc (Emscripten gcc/clang-like replacement) X.X.XNote: You need to run source ./emsdk_env.sh in each new terminal session, or add it to your shell profile (~/.bashrc, ~/.zshrc, etc.)
# Navigate to project root
cd /path/to/cpp-react-wasm-typing-tutor
# Install all npm packages
npm install
# This installs:
# - react, react-dom
# - react-router-dom
# - @supabase/supabase-js
# - recharts
# - react-icons
# - vite
# - tailwindcss
# - And all their dependencies# Navigate to build directory
cd build
# Clean previous builds (optional)
make clean
# Build C++ to WebAssembly
make
# Output:
# - ../public/typing.js (JavaScript loader)
# - ../public/typing.wasm (WebAssembly binary)Alternative: Use the build script
# From project root
./build/build.sh
# This script:
# 1. Checks for Emscripten
# 2. Cleans old builds
# 3. Compiles C++ to WebAssembly
# 4. Installs npm dependenciesIf you want leaderboard and profile features:
-
Create Supabase Account
- Go to supabase.com
- Sign up for free account
- Create new project
-
Get Credentials
- Go to Project Settings → API
- Copy:
- Project URL (e.g.,
https://xxxxx.supabase.co) - Anon/Public Key (long string)
- Project URL (e.g.,
-
Create Environment File
# Copy example file cp .env.example .env # Edit .env file VITE_SUPABASE_URL=https://your-project.supabase.co VITE_SUPABASE_ANON_KEY=your-anon-key-here
-
Create Database Table
- Go to SQL Editor in Supabase dashboard
- Click "New Query"
- Copy contents of
supabase-schema.sql - Paste and click "Run"
- Table
leaderboardwill be created with RLS policies
# From project root
npm run dev
# Output:
# ➜ Local: http://localhost:5173/
# ➜ Network: use --host to exposeOpen http://localhost:5173 in your browser!
- Node.js installed (
node --version) - Emscripten installed (
emcc --version) - npm packages installed (
node_modules/exists) - WebAssembly built (
public/typing.jsandtyping.wasmexist) - Development server runs (
npm run dev) - Browser loads app (no console errors)
- Supabase configured (if using leaderboard)
cpp-react-wasm-typing-tutor/
│
├── cpp/ # C++ Source Files
│ ├── bindings.cpp # C++ to JavaScript bridge
│ ├── TextGenerator.cpp # Abstract base class
│ ├── RandomWordGenerator.cpp # Random word generator
│ ├── SentenceGenerator.cpp # Sentence generator
│ ├── MixedCaseGenerator.cpp # Mixed case generator
│ ├── TypingSession.cpp # Typing accuracy tracking
│ └── Timer.cpp # High-precision timer
│
├── build/ # Build Scripts
│ ├── Makefile # Emscripten build configuration
│ └── build.sh # Automated build script
│
├── src/ # React Source Files
│ ├── main.jsx # Application entry point
│ ├── App.jsx # Router configuration
│ ├── wasmLoader.js # WebAssembly loader & bridge
│ ├── index.css # Global styles
│ │
│ ├── pages/ # Page Components
│ │ ├── TypingTest.jsx # Main typing interface
│ │ ├── LeaderboardPage.jsx # Global leaderboard
│ │ └── ProfilePage.jsx # User profile with graphs
│ │
│ ├── components/ # Reusable Components
│ │ ├── UsernameButton.jsx # Username display & navigation
│ │ ├── NameInputModal.jsx # Results & username input modal
│ │ ├── Leaderboard.jsx # Leaderboard component (legacy)
│ │ └── Profile.jsx # Profile component (legacy)
│ │
│ └── lib/ # Utility Libraries
│ └── supabase.js # Supabase client configuration
│
├── public/ # Static Assets
│ ├── favicon.png # Site favicon
│ ├── typing.js # Compiled WASM JS (generated)
│ └── typing.wasm # Compiled WASM binary (generated)
│
├── .env.example # Environment variables template
├── .gitignore # Git ignore patterns
├── index.html # HTML entry point
├── package.json # Node.js dependencies
├── package-lock.json # Locked dependency versions
├── vite.config.js # Vite configuration
├── tailwind.config.js # Tailwind CSS configuration
├── postcss.config.js # PostCSS configuration
├── vercel.json # Vercel deployment config
├── supabase-schema.sql # Database schema
├── README.md # This file
├── ARCHITECTURE.md # Detailed architecture docs
└── QUICKSTART.md # Quick start guide
| File | Lines | Purpose | Key Classes/Functions |
|---|---|---|---|
bindings.cpp |
112 | C++ to JavaScript bridge | generateText(), startSession(), updateInput(), etc. |
TextGenerator.cpp |
16 | Abstract base class | TextGenerator (virtual) |
RandomWordGenerator.cpp |
77 | Random word generation | RandomWordGenerator |
SentenceGenerator.cpp |
74 | Sentence generation | SentenceGenerator |
MixedCaseGenerator.cpp |
97 | Mixed case generation | MixedCaseGenerator |
TypingSession.cpp |
72 | Typing tracking | TypingSession |
Timer.cpp |
44 | Time measurement | Timer |
| File | Lines | Purpose | Key Features |
|---|---|---|---|
main.jsx |
14 | App entry | React root, router setup |
App.jsx |
24 | Router | Route definitions |
wasmLoader.js |
59 | WASM bridge | Load module, wrap functions |
pages/TypingTest.jsx |
776 | Main interface | Typing test, real-time stats |
pages/LeaderboardPage.jsx |
188 | Leaderboard | Top 100 rankings |
pages/ProfilePage.jsx |
816 | User profile | Graphs, heatmap, history |
components/UsernameButton.jsx |
54 | Username display | Navigation, localStorage |
components/NameInputModal.jsx |
163 | Results modal | Score display, username input |
| File | Purpose | Key Settings |
|---|---|---|
package.json |
Dependencies | React, Vite, Supabase, etc. |
vite.config.js |
Build config | React plugin, server settings |
tailwind.config.js |
CSS theme | Colors, fonts, animations |
vercel.json |
Deployment | SPA routing rewrite |
supabase-schema.sql |
Database | Table schema, RLS policies |
Browser loads index.html
│
▼
React app initializes (main.jsx)
│
├─> Router setup (App.jsx)
│ └─> Defines routes: /, /leaderboard, /profile
│
└─> WASM module loads (wasmLoader.js)
│
├─> Loads /typing.js (Emscripten loader)
├─> Downloads /typing.wasm (WebAssembly binary)
├─> Initializes WebAssembly module
└─> Creates JavaScript function wrappers
User clicks "Start Test" or presses Enter
│
▼
TypingTest.jsx: startTest()
│
├─> wasm.setGeneratorType(RANDOM_WORDS)
│ └─> C++: Creates RandomWordGenerator instance
│
├─> wasm.generateText(25)
│ │
│ ├─> JavaScript: wasmLoader.js
│ │ └─> Calls C++ function via cwrap
│ │
│ └─> C++: RandomWordGenerator::generateText(25)
│ ├─> Selects 25 random words
│ ├─> Joins with spaces
│ └─> Returns: "apple green river monkey..."
│
└─> setTargetText("apple green river...")
└─> React: Renders text on screen
User types first character: "a"
│
▼
TypingTest.jsx: handleInputChange("a")
│
├─> if (!hasStartedTyping) {
│ │ wasm.startSession(targetText)
│ │ │
│ │ ├─> C++: TypingSession::startSession(text)
│ │ │ └─> Stores target text
│ │ │
│ │ └─> C++: Timer::start()
│ │ └─> Records start time
│ │
│ └─> setHasStartedTyping(true)
│
├─> wasm.updateInput("a")
│ │
│ └─> C++: TypingSession::updateInput("a")
│ ├─> Compares "a" with target[0]
│ ├─> correctChars = 1 (match)
│ └─> totalChars = 1
│
├─> wasm.getAccuracy()
│ └─> C++: Returns 100.0 (1/1 * 100)
│
└─> setAccuracy(100)
└─> React: Updates UI
setInterval (every 100ms)
│
├─> wasm.getElapsedSeconds()
│ └─> C++: Timer::elapsedSeconds()
│ └─> Returns: 2.5 seconds
│
├─> wasm.getWPM(2.5)
│ └─> C++: TypingSession::wpm(2.5)
│ ├─> correctChars = 15
│ ├─> minutes = 2.5 / 60 = 0.0417
│ ├─> wpm = (15 / 5) / 0.0417 = 72
│ └─> Returns: 72
│
└─> setWpm(72), setTimer(2.5)
└─> React: Updates display
User finishes typing OR 60 seconds pass
│
▼
TypingTest.jsx: finishTest()
│
├─> Calculate final stats
│ ├─> finalWpm = wasm.getWPM(elapsed)
│ ├─> finalAccuracy = wasm.getAccuracy()
│ └─> finalTime = wasm.getElapsedSeconds()
│
├─> Save to Supabase (if username set)
│ │
│ └─> supabase.from('leaderboard').insert([{
│ username: "John",
│ wpm: 72,
│ accuracy: 96.5,
│ time: 45.2
│ }])
│
└─> setShowNameModal(true)
└─> React: Shows results modal
// In bindings.cpp
char* generateText(int wordCount) {
std::string text = textGen->generateText(wordCount);
// Allocate memory on heap (must be freed by JavaScript)
char* result = (char*)malloc(text.length() + 1);
strcpy(result, text.c_str());
return result; // JavaScript receives pointer
}// In wasmLoader.js
const generateText = (wordCount) => {
// 1. Call C++ function (returns pointer)
const ptr = generateTextPtr(wordCount);
// 2. Convert pointer to JavaScript string
const str = wasmModule.UTF8ToString(ptr);
// 3. CRITICAL: Free C++ memory immediately
wasmModule._free(ptr);
// 4. Return JavaScript string (garbage collected)
return str;
};Why This Matters:
- C++ memory is NOT garbage collected
- Must manually free allocated memory
- Memory leaks occur if not freed
- Best practice: Convert and free immediately
Purpose: Polymorphic interface for text generation
Design Pattern: Strategy Pattern + Inheritance
class TextGenerator {
public:
virtual string generateText(int count) = 0;
virtual ~TextGenerator() {}
};Implementations:
RandomWordGenerator: Random words (5-6 letters)SentenceGenerator: Complete sentencesMixedCaseGenerator: Words with random capitalization
Benefits:
- Easy to add new generator types
- Polymorphic behavior
- Clean separation of concerns
Purpose: Tracks typing accuracy and calculates statistics
Key Data:
targetText: What user should typeuserInput: What user actually typedcorrectChars: Count of correct characterstotalChars: Total characters typed
Key Methods:
void updateInput(string typed) {
userInput = typed;
totalChars = typed.length();
correctChars = 0;
// Character-by-character comparison
int minLength = min(targetText.length(), typed.length());
for (int i = 0; i < minLength; i++) {
if (targetText[i] == typed[i]) {
correctChars++;
}
}
}
double accuracy() {
if (totalChars == 0) return 100.0;
return (correctChars / totalChars) * 100.0;
}
int wpm(double secondsElapsed) {
if (secondsElapsed <= 0) return 0;
double wordSize = 5.0; // Standard: 5 chars per word
double minutes = secondsElapsed / 60.0;
double wpmValue = (correctChars / wordSize) / minutes;
return static_cast<int>(round(wpmValue));
}Algorithm:
- Compare character-by-character
- Count matches
- Calculate percentage
- Use standard WPM formula:
(chars / 5) / minutes
Purpose: High-precision time measurement
Implementation:
void start() {
startTime = clock(); // High-precision clock
isRunning = true;
}
double elapsedSeconds() {
if (isRunning) {
clock_t current = clock();
return static_cast<double>(current - startTime) / CLOCKS_PER_SEC;
}
return 0.0;
}Precision: Uses std::clock() for millisecond precision
Purpose: Expose C++ functions to JavaScript
Key Functions:
| Function | C++ Implementation | Returns | Purpose |
|---|---|---|---|
setGeneratorType(int) |
Creates generator instance | void |
Switch text type |
generateText(int) |
TextGenerator::generateText() |
char* |
Generate text |
startSession(char*) |
TypingSession::startSession() + Timer::start() |
void |
Start test |
updateInput(char*) |
TypingSession::updateInput() |
void |
Update input |
getAccuracy() |
TypingSession::accuracy() |
double |
Get accuracy % |
getWPM(double) |
TypingSession::wpm() |
int |
Get WPM |
resetSession() |
Reset all state | void |
Reset test |
getElapsedSeconds() |
Timer::elapsedSeconds() |
double |
Get time |
Memory Management:
generateText()allocates withmalloc()- must be freed- Other functions use stack allocation or no return
Purpose: Main typing interface
State Management:
const [wasm, setWasm] = useState(null); // WASM functions
const [targetText, setTargetText] = useState(''); // Text to type
const [userInput, setUserInput] = useState(''); // User input
const [isTestActive, setIsTestActive] = useState(false);
const [hasStartedTyping, setHasStartedTyping] = useState(false);
const [timer, setTimer] = useState(0); // Elapsed time
const [accuracy, setAccuracy] = useState(100); // Accuracy %
const [wpm, setWpm] = useState(0); // Words per minute
const [generatorType, setGeneratorType] = useState(RANDOM_WORDS);Key Functions:
startTest(): Initialize test, generate texthandleInputChange(): Process user input, update statsfinishTest(): Calculate final stats, save to databaserestartTest(): Reset and start new testgoToHomeScreen(): Return to text type selection
Real-Time Updates:
useEffect(() => {
if (isTestActive && hasStartedTyping && !isTestComplete) {
intervalRef.current = setInterval(() => {
const elapsed = wasm.getElapsedSeconds();
setTimer(elapsed);
const currentWpm = wasm.getWPM(elapsed);
setWpm(currentWpm);
if (elapsed >= 60) {
finishTest();
}
}, 100); // Update every 100ms
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isTestActive, hasStartedTyping, isTestComplete, wasm]);Purpose: User statistics and progress visualization
Features:
- Stats Summary: Best WPM, accuracy, total sessions
- Progress Graph: Line chart with Recharts
- Activity Heatmap: GitHub-style contribution graph
- Session History: Filterable table
Graph Data Processing:
const getGraphData = () => {
const filtered = getFilteredSessions();
const dataByDate = {};
// Group sessions by date
filtered.forEach(session => {
const dateKey = new Date(session.created_at).toISOString().split('T')[0];
if (!dataByDate[dateKey]) {
dataByDate[dateKey] = { wpm: [], accuracy: [] };
}
dataByDate[dateKey].wpm.push(session.wpm);
dataByDate[dateKey].accuracy.push(session.accuracy);
});
// Calculate daily averages
const chartData = [];
for (let i = days - 1; i >= 0; i--) {
const date = new Date(now);
date.setDate(date.getDate() - i);
const dateKey = date.toISOString().split('T')[0];
if (dataByDate[dateKey]) {
const avgWpm = dataByDate[dateKey].wpm.reduce((a, b) => a + b, 0) / dataByDate[dateKey].wpm.length;
const avgAcc = dataByDate[dateKey].accuracy.reduce((a, b) => a + b, 0) / dataByDate[dateKey].accuracy.length;
chartData.push({ date: dateKey, wpm: Math.round(avgWpm), accuracy: parseFloat(avgAcc.toFixed(1)) });
}
}
return chartData;
};Purpose: Global rankings
Score Comparison Logic:
const isBetterScore = (newScore, existingScore) => {
// Primary: WPM (higher is better)
if (newScore.wpm > existingScore.wpm) return true;
if (newScore.wpm < existingScore.wpm) return false;
// Secondary: Accuracy (higher is better)
if (newScore.accuracy > existingScore.accuracy) return true;
if (newScore.accuracy < existingScore.accuracy) return false;
// Tertiary: Time (lower is better)
return newScore.time < existingScore.time;
};
// Group by username, keep best score
const userMap = new Map();
data.forEach(entry => {
const existing = userMap.get(entry.username);
if (!existing || isBetterScore(entry, existing)) {
userMap.set(entry.username, entry);
}
});
// Sort and get top 100
const topUsers = Array.from(userMap.values())
.sort((a, b) => {
if (b.wpm !== a.wpm) return b.wpm - a.wpm;
if (b.accuracy !== a.accuracy) return b.accuracy - a.accuracy;
return a.time - b.time;
})
.slice(0, 100);// From tailwind.config.js
colors: {
bg: {
primary: '#323437', // Main background
secondary: '#2c2e31', // Card backgrounds
tertiary: '#262729', // Hover states
},
text: {
primary: '#d1d0c5', // Main text
secondary: '#646669', // Secondary text
tertiary: '#72757e', // Muted text
},
accent: '#e2b714', // Gold/yellow accent
correct: '#e2b714', // Correct characters
incorrect: '#ca4754', // Incorrect characters
}- Font Family: JetBrains Mono, Fira Code, Courier New, monospace
- Font Sizes: Responsive (text-sm to text-3xl)
- Font Weights: 400 (normal), 500 (medium), 600 (semibold), 700 (bold)
animations: {
'fade-in': 'fadeIn 0.4s ease-in',
'slide-down': 'slideDown 0.3s ease-out',
'fade-in-up': 'fadeInUp 0.4s ease-out',
'blink': 'blink 1s infinite',
'correct-pulse': 'correctPulse 0.3s ease-out',
'incorrect-shake': 'incorrectShake 0.4s ease-out',
}- Mobile:
< 640px(sm) - Tablet:
640px - 1024px(md, lg) - Desktop:
> 1024px(xl, 2xl)
┌─────────────────────────────────────────┐
│ Header (Username | Logo | Leaderboard) │
├─────────────────────────────────────────┤
│ Stats Bar (Time | WPM | Accuracy) │
├─────────────────────────────────────────┤
│ Generator Selector (Words/Sentences/ │
│ Mixed Case) │
├─────────────────────────────────────────┤
│ Language Label │
├─────────────────────────────────────────┤
│ Text Display Area │
│ (Target text with color coding) │
├─────────────────────────────────────────┤
│ Controls (Start/Restart/Change Type) │
├─────────────────────────────────────────┤
│ Footer (Keyboard shortcuts) │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Header (Back | Logo | Leaderboard) │
├─────────────────────────────────────────┤
│ Profile Header (Username | Rank) │
├─────────────────────────────────────────┤
│ Stats Cards (Best WPM | Accuracy | │
│ Total Sessions) │
├─────────────────────────────────────────┤
│ Progress Graph (WPM & Accuracy over │
│ time) │
├─────────────────────────────────────────┤
│ Activity Heatmap (GitHub-style) │
├─────────────────────────────────────────┤
│ Session History Table (Filterable) │
└─────────────────────────────────────────┘
┌─────────────┐
│ User │
└──────┬──────┘
│ Types "a"
▼
┌─────────────────┐
│ TypingTest.jsx │
│ handleInputChange() │
└──────┬──────────┘
│ wasm.updateInput("a")
▼
┌─────────────────┐
│ wasmLoader.js │
│ updateInput() │
└──────┬──────────┘
│ cwrap("updateInput", ...)
▼
┌─────────────────┐
│ typing.js │
│ (Emscripten) │
└──────┬──────────┘
│ WebAssembly Call
▼
┌─────────────────┐
│ bindings.cpp │
│ updateInput() │
└──────┬──────────┘
│ TypingSession::updateInput()
▼
┌─────────────────┐
│ TypingSession │
│ - Compares chars│
│ - Updates stats │
└──────┬──────────┘
│ Return (void)
▼
┌─────────────────┐
│ wasmLoader.js │
│ (no return) │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ TypingTest.jsx │
│ wasm.getAccuracy() │
└──────┬──────────┘
│
▼ (same path back)
┌─────────────────┐
│ TypingSession │
│ accuracy() │
│ Returns: 100.0 │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ TypingTest.jsx │
│ setAccuracy(100) │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ React Render │
│ UI Updates │
└─────────────────┘
User completes test
│
▼
TypingTest.jsx: finishTest()
│
├─> Calculate final stats
│ ├─> finalWpm = 72
│ ├─> finalAccuracy = 96.5
│ └─> finalTime = 45.2
│
└─> Save to Supabase
│
├─> Check if username exists
│ └─> localStorage.getItem('typingTutor_username')
│
├─> Check if score is better
│ └─> Query existing scores
│ └─> Compare with best score
│
└─> Insert into database
│
└─> supabase.from('leaderboard').insert([{
username: "John",
wpm: 72,
accuracy: 96.5,
time: 45.2,
created_at: "2024-12-13T16:30:00Z"
}])
│
▼
Supabase Database
│
├─> Row Level Security check
├─> Insert into leaderboard table
└─> Return success/error
-
Character-by-Character Comparison
- Uses
std::min()to avoid out-of-bounds - Single pass through strings
- O(n) time complexity
- Uses
-
Efficient String Building
- Uses
std::ostringstreamfor concatenation - Avoids multiple string copies
- Pre-allocates capacity when possible
- Uses
-
Memory Management
- Stack allocation for temporary strings
- Heap allocation only for returns to JavaScript
- Immediate freeing in JavaScript
-
State Updates
- Batched updates where possible
- Minimal re-renders
- useRef for non-reactive values
-
Real-Time Updates
- 100ms interval (balance of responsiveness and performance)
- Cleans up intervals properly
- Conditional updates (only when active)
-
Component Optimization
- Memoization where beneficial
- Conditional rendering
- Lazy loading (future improvement)
-
Compilation Flags
-O2: Optimized for size and speedMODULARIZE=1: Module patternALLOW_MEMORY_GROWTH=1: Dynamic memory
-
Function Wrapping
cwrap()for efficient calls- Type conversions handled by Emscripten
- Minimal overhead
-
Indexing
- Index on
usernamefor fast lookups - Index on
wpm DESCfor sorting - Composite indexes (future improvement)
- Index on
-
Query Optimization
- Only fetch needed columns
- Order by indexed columns
- Limit results (top 100)
-
Caching
- Client-side caching of user data
- localStorage for username
- React state for session data
-
Make Changes
# Edit C++ files vim cpp/RandomWordGenerator.cpp # Edit React files vim src/pages/TypingTest.jsx
-
Rebuild if C++ Changed
cd build make cd ..
-
Start Dev Server
npm run dev
-
Test in Browser
- Open http://localhost:5173
- Hot reload for React changes
- Refresh for WASM changes
# Compile with debug symbols
emcc -g -O0 cpp/bindings.cpp -o public/typing.js
# Use browser DevTools
# - Sources tab → typing.wasm
# - Set breakpoints
# - Step through code// Use React DevTools browser extension
// - Component tree
// - Props/state inspection
// - Performance profiling
// Console logging
console.log('WASM:', wasm);
console.log('State:', { wpm, accuracy, timer });// In browser console
Module._generateText(25); // Direct WASM call
Module.UTF8ToString(ptr); // Convert pointer- Start test works
- Timer starts on first keystroke
- Accuracy updates correctly
- WPM calculates correctly
- Test finishes on completion
- Test finishes after 60 seconds
- Restart works
- Change text type works
- Scores save to database
- Leaderboard displays correctly
- Profile graphs render
- Mobile responsive
// Measure WASM call performance
console.time('generateText');
const text = wasm.generateText(25);
console.timeEnd('generateText');
// Measure render performance
// Use React DevTools Profiler# 1. Build WebAssembly
cd build
make
cd ..
# 2. Start dev server
npm run dev# 1. Build WebAssembly (optimized)
cd build
make clean
make
cd ..
# 2. Build React app
npm run build
# Output: dist/ folder-
Connect Repository
- Push to GitHub
- Import in Vercel
- Connect repository
-
Configure Environment Variables
VITE_SUPABASE_URL=https://xxx.supabase.co VITE_SUPABASE_ANON_KEY=xxx -
Build Settings
- Build Command:
cd build && make && cd .. && npm run build - Output Directory:
dist - Install Command:
npm install
- Build Command:
-
Deploy
- Vercel auto-deploys on push
- Or manually trigger deployment
- Connect Repository
- Build Settings:
- Build command:
cd build && make && cd .. && npm run build - Publish directory:
dist
- Build command:
- Environment Variables: Same as Vercel
- Deploy
- Build locally
- Push dist/ to gh-pages branch
- Enable GitHub Pages in repo settings
# In Makefile, use -O3 for maximum optimization
EMCC_FLAGS = -O3 \
-s EXPORTED_FUNCTIONS='[...]' \
...// vite.config.js
export default defineConfig({
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
},
},
},
});Create .env file:
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key-herevite.config.js:
export default defineConfig({
plugins: [react()],
server: {
fs: {
allow: ['..'] // Allow serving files from parent directory
}
},
optimizeDeps: {
exclude: ['../typing.js'] // Don't optimize WASM loader
},
build: {
commonjsOptions: {
include: [/typing\.js$/, /node_modules/],
transformMixedEsModules: true
}
}
});tailwind.config.js:
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: { /* custom colors */ },
fontFamily: { /* custom fonts */ },
animations: { /* custom animations */ },
},
},
plugins: [],
}Error: emcc: command not found
Solution:
# Activate Emscripten
source ~/emsdk/emsdk_env.sh
# Or add to ~/.bashrc or ~/.zshrc
echo 'source ~/emsdk/emsdk_env.sh' >> ~/.bashrcError: Failed to load C++ WebAssembly module
Solutions:
- Check
public/typing.jsandtyping.wasmexist - Check browser console for errors
- Verify file paths in
wasmLoader.js - Check CORS if serving from different domain
Error: Cannot enlarge memory arrays
Solution:
- Already handled with
ALLOW_MEMORY_GROWTH=1 - If persists, increase
INITIAL_MEMORYin Makefile
Error: Leaderboard is not configured
Solutions:
- Check
.envfile exists - Verify environment variables are set
- Check Supabase project is active
- Verify table exists in database
Error: make: *** [target] Error 1
Solutions:
- Check Emscripten is activated
- Verify C++ syntax is correct
- Check file paths in Makefile
- Clean and rebuild:
make clean && make
-
Check Browser Console
- Look for JavaScript errors
- Check network tab for failed requests
- Inspect WebAssembly module loading
-
Check React DevTools
- Component tree
- State values
- Props flow
-
Check Network Tab
- Verify
typing.jsandtyping.wasmload - Check Supabase API calls
- Look for 404 errors
- Verify
-
Use Console Logging
console.log('WASM:', wasm); console.log('State:', { wpm, accuracy });
- WebAssembly.org - Official site
- MDN WebAssembly Guide - Comprehensive guide
- WebAssembly by Example - Code examples
- Emscripten Documentation - Official docs
- Emscripten Tutorial - Getting started
- Emscripten API Reference - API docs
- React Documentation - Official docs
- React Router - Routing library
- React Hooks - Hooks reference
- cppreference.com - C++ reference
- Learn C++ - Tutorial site
- C++ Core Guidelines - Best practices
- Supabase Documentation - Official docs
- Supabase JavaScript Client - JS client
- Fork the Repository
- Create Feature Branch:
git checkout -b feature/amazing-feature - Make Changes
- Test Thoroughly
- Commit Changes:
git commit -m 'Add amazing feature' - Push to Branch:
git push origin feature/amazing-feature - Open Pull Request
- C++: Follow Google C++ Style Guide
- JavaScript: Use ESLint/Prettier
- Comments: Keep code self-documenting, minimal comments
- Test all features before submitting
- Test on multiple browsers
- Test responsive design
This project is for educational purposes. Feel free to use, modify, and learn from it!
- Emscripten Team - For the amazing C++ to WebAssembly compiler
- React Team - For the excellent UI framework
- Supabase Team - For the backend-as-a-service platform
- Tailwind CSS Team - For the utility-first CSS framework
For questions, issues, or contributions:
- Open an issue on GitHub
- Check the documentation
- Review the code comments
Happy Typing! 🚀
Last Updated: December 2024