A customizable typing game for learning any topic. Fork this template and populate it with your own curriculum - medical terms, language vocabulary, coding shortcuts, or anything else you want to learn by typing.
- Retro Arcade Theme - 8-bit pixel art aesthetic with neon colors
- Multiple Game Modes - Quiz mode and timed race mode
- Smart Task Selection - Category-balanced algorithm ensures chapter diversity and difficulty mix
- Progress Tracking - XP system, levels, and streaks saved locally (shared via React context, syncs across tabs)
- Study Mode - Review concepts before playing
- AI Challenge (Optional) - OpenAI-powered quiz generation
- Shortcut Hints Overlay - Contextual keyboard shortcut hints for new users, auto-dismisses on first keystroke
- Sound Effects - Procedural 8-bit audio
- Mobile Friendly - Responsive design works on all devices
- 100% Customizable - Change curriculum, branding, and features via JSON
# Clone the template
git clone https://github.com/YOUR_USERNAME/typemaster-template.git my-typing-game
cd my-typing-game
# Install dependencies
npm install
# Start development server
npm run devOpen http://localhost:3000 to see your game.
Edit the main config file to customize your game:
{
"siteName": "MedTerms",
"tagline": "Learn medical terminology by typing",
"description": "Master medical vocabulary with this typing game",
"theme": {
"primary": "#00FF9F",
"secondary": "#00D4FF",
"accent": "#FF6B9D"
},
"features": {
"soundEffects": true,
"studyMode": true,
"aiChallenge": true,
"leaderboard": true
},
"gameModes": {
"quiz": {
"enabled": true,
"description": "Type the term that matches the definition"
},
"race": {
"enabled": true,
"duration": 60,
"description": "Type as many terms as possible in 60 seconds"
}
}
}Create a new JSON file in curriculum/chapters/:
{
"id": "anatomy-basics",
"title": "Anatomy Basics",
"description": "Learn the fundamental parts of the human body",
"icon": "ai-foundations",
"order": 1,
"concepts": [
{
"id": "anat-001",
"term": "femur",
"definition": "The thigh bone, longest bone in the human body",
"difficulty": "easy"
},
{
"id": "anat-002",
"term": "patella",
"definition": "The kneecap, a small bone protecting the knee joint",
"difficulty": "medium"
}
],
"levels": [
{
"id": 1,
"name": "Bones 101",
"concepts": ["anat-001", "anat-002"],
"requiredXp": 0,
"gameMode": "quiz"
}
]
}Then import it in src/lib/curriculum-loader.ts:
import anatomyBasics from "../../curriculum/chapters/anatomy-basics.json";
const chapterFiles = [
anatomyBasics,
// Add more chapters here
];Use any of these built-in icons in your chapter's icon field:
| Icon Name | Color | Best For |
|---|---|---|
ai-foundations |
Green | General learning, intro chapters |
full-stack |
Pink | Layered concepts, stacks |
machine-learning |
Yellow | Data, analytics |
code-practice |
Lime | Programming, code |
databases |
Emerald | Storage, data |
security-auth |
Rose | Security, passwords |
containers-docker |
Blue | DevOps, infrastructure |
cloud-platforms |
Violet | Cloud, services |
Unknown icon names gracefully fall back to a question mark icon.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier (used in URLs) |
title |
string | Yes | Display title |
description |
string | Yes | Short description shown on chapter card |
icon |
string | Yes | Icon name from built-in set |
order |
number | Yes | Display order (1, 2, 3...) |
concepts |
array | Yes | Array of concept objects |
levels |
array | Yes | Array of level objects |
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier (referenced in levels) |
term |
string | Yes | The word/phrase to type |
definition |
string | Yes | The definition shown as a prompt |
difficulty |
string | Yes | "easy", "medium", or "hard" |
| Field | Type | Required | Description |
|---|---|---|---|
id |
number | Yes | Level number (1, 2, 3...) |
name |
string | Yes | Level display name |
concepts |
array | Yes | Array of concept IDs for this level |
requiredXp |
number | Yes | XP needed to unlock (0 for first level) |
gameMode |
string | Yes | "quiz" or "race" |
TypeMaster uses a category-balanced selection algorithm (diversePick() in src/utils/task.ts) instead of naive random shuffling. This ensures every round feels varied and challenging:
| Guarantee | How It Works |
|---|---|
| Chapter balance | Concepts are drawn proportionally via round-robin across all unlocked chapters — no single chapter can dominate a round |
| Difficulty floor | At least 2 hard-difficulty concepts per round (when available), preventing all-easy streaks |
| True shuffle | Final selection uses Fisher-Yates shuffle so presentation order is unpredictable |
| Graceful fallback | If the pool is smaller than the requested count, the entire pool is returned shuffled |
Used by Race Mode and AI Explain Mode. Quiz Mode uses level-defined concept lists already curated by the curriculum author.
import { diversePick } from "@/utils/task";
// Pick 10 concepts balanced across chapters, at least 2 hard
const round = diversePick(concepts, 10);
// Custom difficulty floor (at least 3 hard concepts)
const hardRound = diversePick(concepts, 10, 3);All game state lives in localStorage and flows through a single persistence module. Every read is schema-validated — tampered or corrupted data is rejected and reset to safe defaults. Storage keys are auto-namespaced from config.siteName, so multiple TypeMaster instances on the same domain never collide.
| Function | Purpose | Returns |
|---|---|---|
getProgress() |
Read current user state (XP, level, streaks, scores) | UserProgress (safe defaults if missing/corrupt) |
saveProgress(progress) |
Write full progress object to localStorage | void |
updateProgress(partial) |
Merge partial updates into current state | UserProgress |
addXP(amount) |
Award XP and auto-calculate level (XP_PER_LEVEL threshold) |
UserProgress |
completeLevel(chapterId, levelId) |
Mark a level complete (idempotent — no duplicates) | UserProgress |
updateConceptScore(id, isCorrect) |
Track per-concept correct/incorrect counts and last-seen timestamp | UserProgress |
getConceptsForReview(limit?) |
Get concepts where incorrect > correct, oldest-first (spaced repetition) |
string[] |
updateStreak() |
Increment if played yesterday, reset if gap, hold if same day | UserProgress |
resetProgress() |
Clear all saved state (progress + last-played date) | void |
- SSR safety — Every function guards against
typeof window === "undefined", returning safe defaults during Next.js server rendering - Schema validation —
validateProgressShape()checks every field type, range, and structure before trusting parsed JSON. This blocks localStorage injection attacks where a malicious script writes unexpected types into the progress key - Namespaced keys — Derived from
STORAGE_KEYSinsrc/lib/storage-keys.ts, which prefixes every key with the slugified site name (e.g.,typemaster_progress). Fork the template, changesiteName, and storage isolation is automatic - Immutable updates —
updateProgress()spreads current state with updates, writes, and returns the new state — components always get the post-write value
import { addXP, completeLevel, getConceptsForReview } from "@/lib/storage";
// Award 50 XP after a correct answer
const updated = addXP(50);
console.log(updated.level); // auto-leveled if XP threshold crossed
// Mark chapter "web-dev" level 3 as complete
completeLevel("web-dev", 3);
// Get struggling concepts for a review session
const weak = getConceptsForReview(5);
// → ["concept-id-1", "concept-id-2"] (oldest mistakes first)AI Challenge uses OpenAI to generate unique quiz questions based on concepts you've learned. To enable:
- Keep
features.aiChallenge: truein config.json - Get an OpenAI API key from platform.openai.com
- In the game, go to Settings and enter your API key
- Your key is stored locally in your browser - never sent to any server except OpenAI
To disable AI Challenge completely, set features.aiChallenge: false in config.json.
npm install -g vercel
vercelThis is a standard Next.js 14+ app. Deploy to any platform that supports Next.js:
- Netlify
- AWS Amplify
- Railway
- Self-hosted with
npm run build && npm start
TypeMaster ships with production-grade HTTP security headers configured in next.config.ts:
| Header | Value | Purpose |
|---|---|---|
Content-Security-Policy |
default-src 'self'; connect-src 'self' https://api.openai.com; ... |
Primary XSS mitigation — restricts all resource origins |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Forces HTTPS, prevents downgrade attacks |
Referrer-Policy |
strict-origin-when-cross-origin |
Safe cross-origin requests (fixes YouTube Error 153) |
X-Content-Type-Options |
nosniff |
Prevents MIME-type sniffing |
X-Frame-Options |
SAMEORIGIN |
Blocks clickjacking |
X-XSS-Protection |
1; mode=block |
Legacy XSS filter |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Minimizes API surface |
Defense in depth:
- API key handling — Keys are validated against the
sk-format pattern before storage. OpenAI calls are rate-limited to 10/minute client-side. AI responses are structure-validated before use. - Prompt injection guard — Curriculum content is sanitized (dangerous chars stripped, length-capped) before embedding in AI prompts, preventing prompt manipulation via custom curriculum data.
- localStorage integrity — Progress data is validated against the
UserProgressschema on every read. Tampered or corrupted payloads are rejected and reset to safe defaults. - Error sanitization — API error messages are filtered before reaching the UI, preventing internal detail leakage.
- Next.js 16 - React framework with App Router
- TypeScript - Type safety
- Tailwind CSS 4 - Styling (uses
@theme inline) - Framer Motion - Animations
- Web Audio API - Procedural 8-bit sounds
- LocalStorage - Progress persistence (auto-namespaced per instance)
typemaster-template/
├── curriculum/
│ ├── config.json # Site configuration
│ └── chapters/
│ ├── programming-basics.json # Example chapter
│ └── web-development.json # Example chapter
├── src/
│ ├── app/ # Next.js pages
│ ├── components/ # React components
│ ├── data/curriculum.ts # Re-exports from loader
│ ├── hooks/ # Custom React hooks
│ ├── lib/
│ │ ├── curriculum-loader.ts # Loads JSON curriculum
│ │ ├── storage-keys.ts # Namespaced localStorage key registry (collision-safe)
│ │ ├── storage.ts # Persistence engine — validated reads, XP, streaks, review queue
│ │ └── openai.ts # Optional AI integration
│ ├── utils/
│ │ ├── task.ts # diversePick() — category-balanced concept selection
│ │ └── validation.ts # Shared validation (params, API key, progress)
│ └── types/game.ts # TypeScript types
├── public/ # Static assets
└── package.json
# Run dev server
npm run dev
# Type check
npm run lint
# Build for production
npm run build
# Start production server
npm startContributions welcome! Feel free to:
- Add new icons to
src/components/ui/PixelIcon.tsx - Improve the game mechanics
- Add new features
- Fix bugs
MIT License - feel free to use this for personal or commercial projects.
Made with love. Type your way to mastery!