A modern, production-ready typing speed test web app built with Next.js 14, Supabase, and Framer Motion.
- 🔐 Authentication — Email/password + Google OAuth
- ⌨️ Infinite Practice Mode — Endless typing with real-time WPM & accuracy
- ⏱ 60-Second Test — Timed speed test saved to leaderboard
- 🏆 Leaderboards — Separate boards for both modes, real-time from database
- 🎨 Beautiful UI — Dark mode, smooth animations, responsive design
- ⏸ Smart Pause — Auto-pauses when you switch tabs
- 📊 Progress Tracking — Personal bests, test history, stat dashboard
- Node.js 18+
- A Supabase account (free tier works great)
git clone <your-repo>
cd typecraft
npm install- Go to supabase.com and sign in
- Click New Project
- Choose a name, database password, and region
- Wait for provisioning (~2 minutes)
- In your Supabase dashboard, go to SQL Editor
- Click New Query
- Copy and paste the contents of
supabase/schema.sql - Click Run
This creates:
profilestable (user data)test_resultstable (typing test scores)leaderboard_sixtyview (best WPM in 60s tests)leaderboard_infiniteview (best WPM in infinite mode, min 50 words)- Row Level Security policies
- In Supabase dashboard → Authentication → Providers
- Enable Google
- Go to Google Cloud Console
- Create a project → Enable Google+ API
- Create OAuth 2.0 credentials
- Set authorized redirect URI to:
https://your-project-ref.supabase.co/auth/v1/callback - Copy Client ID and Client Secret back to Supabase
cp .env.example .env.localFill in your values:
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
NEXT_PUBLIC_APP_URL=http://localhost:3000Find these in Supabase: Settings → API
npm run devnpm i -g vercel
vercel- Push your code to GitHub
- Go to vercel.com → New Project
- Import your GitHub repository
- Add environment variables:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYNEXT_PUBLIC_APP_URL(set to your Vercel URL)
- Click Deploy
Update the OAuth redirect URLs in Supabase:
Authentication → URL Configuration:
- Site URL:
https://your-app.vercel.app - Redirect URLs:
https://your-app.vercel.app/auth/callback
typecraft/
├── src/
│ ├── app/ # Next.js App Router pages
│ │ ├── auth/
│ │ │ ├── login/ # Login page
│ │ │ ├── signup/ # Signup page
│ │ │ ├── onboarding/ # New user profile setup
│ │ │ └── callback/ # OAuth callback handler
│ │ ├── dashboard/ # User dashboard
│ │ ├── practice/ # Infinite mode
│ │ ├── test/ # 60-second test
│ │ └── leaderboard/ # Leaderboards
│ │
│ ├── components/
│ │ ├── auth/ # AuthForm component
│ │ ├── layout/ # NavBar, DashboardClient, LeaderboardClient
│ │ ├── typing/ # TypingTest, WordDisplay, StatsBar, TestResults
│ │ └── ui/ # Button, Input, Logo
│ │
│ ├── hooks/
│ │ └── useTypingEngine.ts # Core typing logic
│ │
│ ├── lib/
│ │ ├── supabase.ts # Supabase client
│ │ ├── words.ts # 100+ sentences dataset
│ │ ├── utils.ts # WPM/accuracy calculations
│ │ └── cn.ts # className utility
│ │
│ ├── types/
│ │ └── index.ts # TypeScript types
│ │
│ └── middleware.ts # Auth route protection
│
├── supabase/
│ └── schema.sql # Full database schema
│
├── public/
├── .env.example
├── next.config.js
├── tailwind.config.js
└── package.json
The core typing logic lives in src/hooks/useTypingEngine.ts:
- Word list is built from shuffled sentences (infinite) or a fixed pool (60s)
- Input handling captures keystrokes via a hidden
<input>element - WPM is calculated as:
(correctChars / 5) / minutes - Accuracy is calculated as:
(totalTyped - errors) / totalTyped * 100 - Tab visibility is monitored to auto-pause/resume
- Results are saved to Supabase after each test
| Key | Action |
|---|---|
Tab |
Restart current test |
Esc |
End test (shows results) |
Space |
Advance to next word |
Backspace |
Fix errors or go back to previous word |
| Layer | Tech |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS |
| Animations | Framer Motion |
| Auth & DB | Supabase |
| Fonts | Syne (display), DM Sans (body), JetBrains Mono |
| Deployment | Vercel |
Edit src/lib/words.ts — add sentences to the sentences array. The engine will automatically include them in rotation.
The 60-second mode is hardcoded to 60s. To change it, update useTypingEngine.ts:
const remaining = 60 - totalElapsed // Change 60 to desired seconds
setTimeLeft(60) // Update initial value tooThe infinite mode leaderboard requires ≥50 words. Update in supabase/schema.sql:
AND tr.words_typed >= 50 -- Change this threshold"relation does not exist" errors
→ Make sure you've run the full supabase/schema.sql in the SQL editor
Google login not working → Check that your redirect URL in Google Console matches exactly what's in Supabase
Leaderboard empty
→ The views require the GRANT SELECT statements — re-run the schema
WPM shows 0 → You need to complete at least a few words before the calculation kicks in
MIT