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.


Fetches a question from the keyless Open Trivia DB, decodes HTML entities, shuffles the four answers, and tracks a streak. Next pulls a fresh question.

'use client';

import * as React from 'react';

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

interface Q {
  question: string;
  category: string;
  answers: string[];
  correct: string;
}

function decode(s: string): string {
  if (typeof window === 'undefined') return s;
  return new DOMParser().parseFromString(s, 'text/html').body.textContent ?? s;
}
function shuffle<T>(a: T[]): T[] {
  const r = [...a];
  for (let i = r.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [r[i], r[j]] = [r[j], r[i]];
  }
  return r;
}

export function TriviaGame() {
  const [q, setQ] = React.useState<Q | null>(null);
  const [picked, setPicked] = React.useState<string | null>(null);
  const [score, setScore] = React.useState(0);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(false);

  const load = React.useCallback(() => {
    setLoading(true);
    setPicked(null);
    fetch('https://opentdb.com/api.php?amount=1&type=multiple')
      .then((r) => r.json())
      .then((d) => {
        const item = d.results?.[0];
        if (!item) throw new Error('no question');
        setQ({
          question: decode(item.question),
          category: decode(item.category),
          correct: decode(item.correct_answer),
          answers: shuffle([item.correct_answer, ...item.incorrect_answers].map(decode)),
        });
        setError(false);
      })
      .catch(() => setError(true))
      .finally(() => setLoading(false));
  }, []);

  React.useEffect(() => {
    load();
  }, [load]);

  const pick = (a: string) => {
    if (picked || !q) return;
    setPicked(a);
    if (a === q.correct) setScore((s) => s + 1);
    else setScore(0);
  };

  if (error) return <div className="font-mono text-sm opacity-60">trivia unavailable — try again later</div>;
  if (loading || !q) return <div className="font-mono text-sm opacity-50">loading question…</div>;

  return (
    <div className="flex w-full max-w-lg flex-col items-center gap-5">
      <div className="font-mono text-[11px] uppercase tracking-[0.12em] opacity-55">
        {q.category} · streak {score}
      </div>
      <p className="text-center text-lg font-medium leading-snug">{q.question}</p>
      <div className="grid w-full grid-cols-1 gap-2">
        {q.answers.map((a) => {
          const isCorrect = a === q.correct;
          const state =
            picked == null ? '' : isCorrect ? 'bg-[var(--accent-olive)] text-white' : a === picked ? 'bg-[var(--accent-red)] text-white' : 'opacity-60';
          return (
            <button
              key={a}
              type="button"
              onClick={() => pick(a)}
              disabled={!!picked}
              className={`rounded-md border border-foreground/20 bg-[var(--tile)] px-4 py-3 text-left text-sm font-medium transition-colors ${state}`}
            >
              {a}
            </button>
          );
        })}
      </div>
      {picked ? (
        <Win98Button onClick={load} className="h-10 min-w-32 px-7 text-sm">
          Next question
        </Win98Button>
      ) : null}
    </div>
  );
}
↖ kaspirius