feat(dashboard): refine activity telemetry and maps
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { UserMapLocation } from "./user-world-map";
|
||||
|
||||
export function MapViewToggle({ locations, children }: { locations: UserMapLocation[]; children: ReactNode }) {
|
||||
const [view, setView] = useState<"overview" | "interactive">("overview");
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div aria-label="Map view" className="flex flex-wrap gap-2" role="group">
|
||||
<button aria-controls="map-overview-panel" aria-pressed={view === "overview"} className={`border px-4 py-2 font-mono text-[10px] font-bold uppercase ${view === "overview" ? "border-ink bg-ink text-canvas" : "border-line"}`} id="map-overview-tab" onClick={() => setView("overview")} type="button">World overview</button>
|
||||
<button aria-controls="map-interactive-panel" aria-pressed={view === "interactive"} className={`border px-4 py-2 font-mono text-[10px] font-bold uppercase ${view === "interactive" ? "border-ink bg-ink text-canvas" : "border-line"}`} id="map-interactive-tab" onClick={() => setView("interactive")} type="button">Interactive OpenStreetMap</button>
|
||||
</div>
|
||||
<p className="mt-2 max-w-2xl text-[10px] leading-4 text-muted">Selecting the interactive view requests map tiles from OpenStreetMap, which receives your IP address, the portal origin, and the geographic area being viewed.</p>
|
||||
<div aria-labelledby="map-overview-tab" hidden={view !== "overview"} id="map-overview-panel" role="region">{children}</div>
|
||||
<div aria-labelledby="map-interactive-tab" hidden={view !== "interactive"} id="map-interactive-panel" role="region">
|
||||
{view === "interactive" && <InteractiveMap locations={locations} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InteractiveMap({ locations }: { locations: UserMapLocation[] }) {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!container.current) return;
|
||||
let cancelled = false;
|
||||
let cleanup = () => {};
|
||||
|
||||
void import("leaflet").then((leaflet) => {
|
||||
if (cancelled || !container.current) return;
|
||||
const map = leaflet.map(container.current, { minZoom: 1, worldCopyJump: true }).setView([20, 0], 2);
|
||||
leaflet.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
|
||||
maxZoom: 19,
|
||||
referrerPolicy: "strict-origin-when-cross-origin",
|
||||
}).addTo(map);
|
||||
|
||||
const bounds: [number, number][] = [];
|
||||
for (const user of locations) {
|
||||
const marker = leaflet.circleMarker([user.latitude, user.longitude], {
|
||||
radius: 8,
|
||||
color: "#eee8d8",
|
||||
weight: 3,
|
||||
fillColor: "#a32f1b",
|
||||
fillOpacity: 1,
|
||||
}).addTo(map);
|
||||
const tooltip = document.createElement("span");
|
||||
tooltip.textContent = `${user.nickname} · ${user.location}`;
|
||||
marker.bindTooltip(tooltip, { direction: "top" });
|
||||
const userPath = `/admin/users/${user.userId}`;
|
||||
marker.on("click", () => window.location.assign(userPath));
|
||||
const element = marker.getElement();
|
||||
element?.setAttribute("aria-label", `${user.nickname}, ${user.location}`);
|
||||
element?.setAttribute("role", "link");
|
||||
element?.setAttribute("tabindex", "0");
|
||||
element?.addEventListener("focus", () => marker.openTooltip());
|
||||
element?.addEventListener("blur", () => marker.closeTooltip());
|
||||
element?.addEventListener("keydown", (event) => {
|
||||
const keyboardEvent = event as KeyboardEvent;
|
||||
if (keyboardEvent.key === "Enter" || keyboardEvent.key === " ") {
|
||||
keyboardEvent.preventDefault();
|
||||
window.location.assign(userPath);
|
||||
}
|
||||
});
|
||||
bounds.push([user.latitude, user.longitude]);
|
||||
}
|
||||
if (bounds.length) map.fitBounds(bounds, { padding: [40, 40], maxZoom: 6 });
|
||||
cleanup = () => map.remove();
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cleanup();
|
||||
};
|
||||
}, [locations]);
|
||||
|
||||
return <div aria-label="Interactive map of latest approximate user locations" className="mt-3 h-[32rem] max-h-[70vh] min-h-80 border border-line" ref={container} role="region" />;
|
||||
}
|
||||
Reference in New Issue
Block a user