Code — how this page is built

Under the hood

The actual source of the components on this page, read straight from the repo. Copy it, read it, or open it on GitHub.


TypingSpeedGameview on GitHub ↗

Times from first keystroke to completion, scores WPM (chars/5 ÷ minutes) and per-character accuracy, and colours each target letter as you type. Best persisted to localStorage.

'use client';

import * as React from 'react';

import { Win98Button } from '@/components/ui/win-98-button';

const KEY = 'kaspirius:typing-speed:best';
const SENTENCES = [
  'the quick brown fox jumps over the lazy dog',
  'curious unhurried and a little contrarian',
  'pictures writing small software and noise',
  'everything is grey until you look at it',
  'a name a persona a space for me',
  'discourse is dead please do not respond',
];

export function TypingSpeedGame() {
  const [target, setTarget] = React.useState(SENTENCES[0]);
  const [input, setInput] = React.useState('');
  const [start, setStart] = React.useState<number | null>(null);
  const [result, setResult] = React.useState<{ wpm: number; acc: number } | null>(null);
  const [best, setBest] = React.useState<number | null>(null);

  React.useEffect(() => {
    const b = Number(localStorage.getItem(KEY));
    if (b) setBest(b);
    setTarget(SENTENCES[Math.floor(Math.random() * SENTENCES.length)]);
  }, []);

  const reset = () => {
    setTarget(SENTENCES[Math.floor(Math.random() * SENTENCES.length)]);
    setInput('');
    setStart(null);
    setResult(null);
  };

  const onChange = (v: string) => {
    if (result) return;
    if (start == null && v.length > 0) setStart(performance.now());
    setInput(v);
    if (v.length >= target.length) {
      const elapsedMin = (performance.now() - (start ?? performance.now())) / 60000;
      let correct = 0;
      for (let i = 0; i < target.length; i++) if (v[i] === target[i]) correct++;
      const wpm = Math.max(0, Math.round(target.length / 5 / Math.max(elapsedMin, 1e-6)));
      const acc = Math.round((correct / target.length) * 100);
      setResult({ wpm, acc });
      setBest((b) => {
        const nb = b == null ? wpm : Math.max(b, wpm);
        localStorage.setItem(KEY, String(nb));
        return nb;
      });
    }
  };

  return (
    <div className="flex w-full max-w-xl flex-col items-center gap-6">
      <p className="select-none text-center text-lg leading-relaxed">
        {target.split('').map((ch, i) => {
          const typed = input[i];
          const cls =
            typed == null ? 'opacity-40' : typed === ch ? 'text-[var(--accent-olive)]' : 'text-[var(--accent-red)] underline';
          return (
            <span key={i} className={cls}>
              {ch}
            </span>
          );
        })}
      </p>

      <input
        value={input}
        onChange={(e) => onChange(e.target.value)}
        disabled={!!result}
        autoFocus
        spellCheck={false}
        placeholder="start typing…"
        className="w-full border-b border-foreground/30 bg-transparent px-1 py-2 text-center font-mono text-sm outline-none placeholder:opacity-40"
      />

      <div className="h-6 font-mono text-sm tracking-wide">
        {result ? <span className="font-bold">{result.wpm} wpm · {result.acc}% accuracy</span> : null}
      </div>

      <div className="flex items-center gap-3">
        <Win98Button onClick={reset} className="h-10 min-w-32 px-6 text-sm">
          New sentence
        </Win98Button>
      </div>
      <div className="font-mono text-[11px] uppercase tracking-[0.12em] opacity-50">
        best {best != null ? `${best} wpm` : '—'}
      </div>
    </div>
  );
}
↖ kaspirius