feat(dashboard): refine activity telemetry and maps
This commit is contained in:
@@ -4,6 +4,7 @@ import { geoEquirectangular, geoPath } from "d3-geo";
|
||||
import { feature } from "topojson-client";
|
||||
import countriesTopologyJson from "world-atlas/countries-110m.json";
|
||||
import Link from "next/link";
|
||||
import { MapViewToggle } from "./map-view-toggle";
|
||||
|
||||
const WIDTH = 1_000;
|
||||
const HEIGHT = 500;
|
||||
@@ -16,6 +17,7 @@ export interface UserMapLocation {
|
||||
userId: string;
|
||||
name: string;
|
||||
discordUsername: string;
|
||||
nickname: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
location: string;
|
||||
@@ -35,7 +37,8 @@ export function UserWorldMap({ locations, unavailableCount }: { locations: UserM
|
||||
<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]">
|
||||
<MapViewToggle locations={locations}>
|
||||
<div className="mt-3 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>
|
||||
@@ -52,18 +55,26 @@ export function UserWorldMap({ locations, unavailableCount }: { locations: UserM
|
||||
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]));
|
||||
const tooltipWidth = Math.min(260, Math.max(110, user.nickname.length * 8 + 24));
|
||||
const tooltipX = Math.min(WIDTH - tooltipWidth - 4, Math.max(4, x - tooltipWidth / 2));
|
||||
const tooltipY = y > 46 ? y - 38 : y + 18;
|
||||
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>
|
||||
<a aria-label={`${user.nickname}, ${user.location}, last seen ${user.observedAt.toISOString()}`} className="map-marker-link" 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">
|
||||
<title>{user.nickname} · {user.location} · {user.classification}</title>
|
||||
</circle>
|
||||
<circle className="map-marker" cx={x} cy={y} fill="var(--accent)" pointerEvents="none" r="7" stroke="var(--panel)" strokeWidth="3" />
|
||||
<g aria-hidden="true" className="map-marker-tooltip" pointerEvents="none">
|
||||
<rect fill="var(--ink)" height="28" rx="2" width={tooltipWidth} x={tooltipX} y={tooltipY} />
|
||||
<text dominantBaseline="middle" fill="var(--panel)" fontFamily="var(--font-mono)" fontSize="12" textAnchor="middle" x={tooltipX + tooltipWidth / 2} y={tooltipY + 14}>{user.nickname}</text>
|
||||
</g>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</MapViewToggle>
|
||||
<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">
|
||||
@@ -73,7 +84,7 @@ export function UserWorldMap({ locations, unavailableCount }: { locations: UserM
|
||||
<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.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.nickname}</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>
|
||||
|
||||
Reference in New Issue
Block a user