Files
minecraft-account-manager/apps/web/src/components/user-world-map.tsx
T
dmg 9116107917
CI / validate (push) Successful in 5m20s
Release / release (push) Successful in 10m40s
feat(dashboard): map latest user locations
2026-08-01 19:41:03 -04:00

85 lines
5.4 KiB
TypeScript

import type { FeatureCollection } from "geojson";
import type { GeometryCollection, Topology } from "topojson-specification";
import { geoEquirectangular, geoPath } from "d3-geo";
import { feature } from "topojson-client";
import countriesTopologyJson from "world-atlas/countries-110m.json";
import Link from "next/link";
const WIDTH = 1_000;
const HEIGHT = 500;
const topology = countriesTopologyJson as unknown as Topology<{ countries: GeometryCollection }>;
const countries = feature(topology, topology.objects.countries) as FeatureCollection;
const projection = geoEquirectangular().fitExtent([[1, 1], [WIDTH - 1, HEIGHT - 1]], { type: "Sphere" });
const countryPath = geoPath(projection);
export interface UserMapLocation {
userId: string;
name: string;
discordUsername: string;
latitude: number;
longitude: number;
location: string;
classification: string;
source: string;
observedAt: Date;
}
export function UserWorldMap({ locations, unavailableCount }: { locations: UserMapLocation[]; unavailableCount: number }) {
return (
<section className="mt-8 border border-line bg-panel p-5 shadow-[8px_8px_0_var(--color-shadow)] sm:p-7">
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Latest known location</p>
<h2 className="mt-2 font-display text-3xl font-black uppercase">Community world</h2>
</div>
<p className="max-w-sm text-xs leading-5 text-muted">{locations.length} mapped · {unavailableCount} without coordinates. Locations are approximate IP intelligence, not precise device positions.</p>
</div>
<div className="mt-6 overflow-hidden border border-line bg-[#b9d4d1]">
<svg aria-labelledby="user-world-map-title user-world-map-description" className="h-auto w-full" role="group" viewBox={`0 0 ${WIDTH} ${HEIGHT}`}>
<title id="user-world-map-title">Latest approximate location for registered users</title>
<desc id="user-world-map-description">An open-data world map with one linked marker for every user whose latest geolocated observation has valid coordinates. A complete text list follows.</desc>
<rect fill="#b9d4d1" height={HEIGHT} width={WIDTH} />
<g aria-hidden="true" fill="var(--canvas)" stroke="var(--line)" strokeWidth="0.7">
{countries.features.map((country, index) => {
const path = countryPath(country);
return path ? <path d={path} key={country.id ?? index} /> : null;
})}
</g>
<g>
{locations.map((user) => {
const projected = projection([user.longitude, user.latitude]);
if (!projected) return null;
const x = Math.min(WIDTH - 14, Math.max(14, projected[0]));
const y = Math.min(HEIGHT - 14, Math.max(14, projected[1]));
return (
<a aria-label={`${user.name}, ${user.location}, last seen ${user.observedAt.toISOString()}`} href={`/admin/users/${user.userId}`} key={user.userId}>
<circle className="map-marker-target" cx={x} cy={y} fill="none" pointerEvents="stroke" r="7" stroke="transparent" strokeWidth="24" vectorEffect="non-scaling-stroke" />
<circle className="map-marker" cx={x} cy={y} fill="var(--accent)" pointerEvents="none" r="7" stroke="var(--panel)" strokeWidth="3">
<title>{user.name} · {user.location} · {user.classification}</title>
</circle>
</a>
);
})}
</g>
</svg>
</div>
<p className="mt-2 text-right font-mono text-[9px] text-muted">Map boundaries: Natural Earth, public domain</p>
<details className="mt-5 border-t border-line pt-4">
<summary className="w-fit cursor-pointer font-mono text-[10px] font-bold uppercase underline underline-offset-4">View accessible location list</summary>
<div className="mt-4 overflow-x-auto">
<table className="w-full min-w-[680px] border-collapse text-left text-xs">
<caption className="sr-only">Latest approximate registered-user locations</caption>
<thead className="border-b border-line font-mono text-[9px] uppercase tracking-wider text-muted"><tr><th className="py-3 pr-4" scope="col">User</th><th className="p-3" scope="col">Location</th><th className="p-3" scope="col">Network</th><th className="p-3" scope="col">Source</th><th className="py-3 pl-4" scope="col">Last observed</th></tr></thead>
<tbody className="divide-y divide-line">
{locations.map((user) => <tr key={user.userId}><th className="py-3 pr-4 text-left" scope="row"><Link className="font-mono font-bold underline underline-offset-4" href={`/admin/users/${user.userId}`}>{user.name}</Link><span className="mt-1 block font-mono text-[9px] font-normal text-muted">@{user.discordUsername}</span></th><td className="p-3">{user.location}</td><td className="p-3 font-mono uppercase">{user.classification}</td><td className="p-3">{user.source}</td><td className="py-3 pl-4 font-mono text-[9px]"><time dateTime={user.observedAt.toISOString()}>{user.observedAt.toISOString()}</time></td></tr>)}
{!locations.length && <tr><td className="py-6 text-muted" colSpan={5}>No user observations currently include valid coordinates.</td></tr>}
</tbody>
</table>
</div>
</details>
</section>
);
}