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.


IssTrackerGameview on GitHub ↗

Polls the keyless wheretheiss.at API on a 3s interval and maps lat/lng to an equirectangular SVG, drawing the station plus a trail of recent positions.

'use client';

import * as React from 'react';

const W = 480;
const H = 240;

interface Pos {
  lat: number;
  lng: number;
  alt: number;
  vel: number;
}

export function IssTrackerGame() {
  const [pos, setPos] = React.useState<Pos | null>(null);
  const [trail, setTrail] = React.useState<{ x: number; y: number }[]>([]);
  const [error, setError] = React.useState(false);

  React.useEffect(() => {
    let alive = true;
    const fetchPos = () => {
      fetch('https://api.wheretheiss.at/v1/satellites/25544')
        .then((r) => r.json())
        .then((d) => {
          if (!alive) return;
          const p = { lat: d.latitude, lng: d.longitude, alt: d.altitude, vel: d.velocity };
          setPos(p);
          setError(false);
          setTrail((t) => [...t.slice(-60), { x: ((p.lng + 180) / 360) * W, y: ((90 - p.lat) / 180) * H }]);
        })
        .catch(() => alive && setError(true));
    };
    fetchPos();
    const id = window.setInterval(fetchPos, 3000);
    return () => {
      alive = false;
      window.clearInterval(id);
    };
  }, []);

  if (error && !pos) return <div className="font-mono text-sm opacity-60">ISS position unavailable — try again later</div>;
  if (!pos) return <div className="font-mono text-sm opacity-50">locating the station…</div>;

  const dot = { x: ((pos.lng + 180) / 360) * W, y: ((90 - pos.lat) / 180) * H };

  return (
    <div className="flex w-full max-w-[520px] flex-col items-center gap-5">
      <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} className="w-full max-w-full rounded-md" style={{ background: '#1c2a3a' }}>
        {[0.25, 0.5, 0.75].map((f) => (
          <line key={`h${f}`} x1={0} y1={H * f} x2={W} y2={H * f} stroke="rgba(255,255,255,0.08)" />
        ))}
        {[0.25, 0.5, 0.75].map((f) => (
          <line key={`v${f}`} x1={W * f} y1={0} x2={W * f} y2={H} stroke="rgba(255,255,255,0.08)" />
        ))}
        <polyline
          points={trail.map((p) => `${p.x},${p.y}`).join(' ')}
          fill="none"
          stroke="var(--accent-blue)"
          strokeWidth="1.5"
          opacity="0.6"
        />
        <circle cx={dot.x} cy={dot.y} r="6" fill="var(--accent-red)" />
        <circle cx={dot.x} cy={dot.y} r="11" fill="none" stroke="var(--accent-red)" opacity="0.4" />
      </svg>

      <dl className="grid w-full max-w-sm grid-cols-2 gap-y-2 font-mono text-sm">
        <dt className="opacity-55">Latitude</dt>
        <dd className="text-right">{pos.lat.toFixed(2)}°</dd>
        <dt className="opacity-55">Longitude</dt>
        <dd className="text-right">{pos.lng.toFixed(2)}°</dd>
        <dt className="opacity-55">Altitude</dt>
        <dd className="text-right">{Math.round(pos.alt)} km</dd>
        <dt className="opacity-55">Speed</dt>
        <dd className="text-right">{Math.round(pos.vel).toLocaleString()} km/h</dd>
      </dl>
      <div className="font-mono text-[11px] uppercase tracking-[0.12em] opacity-45">updating every 3 seconds</div>
    </div>
  );
}
↖ kaspirius