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.


CountryGuessGameview on GitHub ↗

Samples four countries from a bundled list each round; the flag is a flagcdn image keyed by the country code. No fetch, so nothing to go stale. Streak counter.

'use client';

import * as React from 'react';

import { Win98Button } from '@/components/ui/win-98-button';
import COUNTRIES from '@/content/countries.json';

interface Country {
  n: string; // name
  c: string; // cca2 (lowercase)
}

const flagUrl = (cc: string) => `https://flagcdn.com/h240/${cc}.png`;

function sample<T>(arr: T[], n: number): T[] {
  const a = [...arr];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a.slice(0, n);
}

export function CountryGuessGame() {
  const all = COUNTRIES as Country[];
  const [options, setOptions] = React.useState<Country[]>([]);
  const [answer, setAnswer] = React.useState<Country | null>(null);
  const [picked, setPicked] = React.useState<string | null>(null);
  const [score, setScore] = React.useState(0);

  const next = React.useCallback(() => {
    const four = sample(all, 4);
    setOptions(four);
    setAnswer(four[Math.floor(Math.random() * 4)]);
    setPicked(null);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

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

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

  if (!answer) return null;

  return (
    <div className="flex w-full max-w-sm flex-col items-center gap-6">
      <div className="font-mono text-[11px] uppercase tracking-[0.12em] opacity-55">streak {score}</div>
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <img
        src={flagUrl(answer.c)}
        alt="flag to identify"
        width={240}
        className="h-36 w-auto rounded-md border border-foreground/20 object-contain shadow-[0_6px_18px_rgba(20,17,11,0.18)]"
      />
      <div className="grid w-full grid-cols-1 gap-2">
        {options.map((o) => {
          const isAnswer = o.c === answer.c;
          const state =
            picked == null ? '' : isAnswer ? 'bg-[var(--accent-olive)] text-white' : o.c === picked ? 'bg-[var(--accent-red)] text-white' : 'opacity-60';
          return (
            <button
              key={o.c}
              type="button"
              onClick={() => pick(o.c)}
              disabled={!!picked}
              className={`rounded-md border border-foreground/20 bg-[var(--tile)] px-4 py-3 text-sm font-medium transition-colors ${state}`}
            >
              {o.n}
            </button>
          );
        })}
      </div>
      {picked ? (
        <Win98Button onClick={next} className="h-10 min-w-32 px-7 text-sm">
          Next flag
        </Win98Button>
      ) : null}
    </div>
  );
}
↖ kaspirius