feat: add map page

This commit is contained in:
William Valentin
2026-02-04 17:42:41 -08:00
parent 4b2a4808b6
commit 8f59d3ba72
4 changed files with 115 additions and 0 deletions

96
apps/web/app/map/page.tsx Normal file
View File

@@ -0,0 +1,96 @@
"use client";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import { useEffect, useRef, useState } from "react";
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
type GeoPoint = {
id: string;
gps_lat: number | null;
gps_lon: number | null;
};
function MapContent({ points, error }: { points: GeoPoint[]; error: string | null }) {
const map = useMap();
const markersRef = useRef<any[]>([]);
useEffect(() => {
markersRef.current.forEach((marker) => marker.remove());
markersRef.current = [];
if (points.length === 0) return;
points.forEach((point) => {
if (point.gps_lat === null || point.gps_lon === null) return;
const marker = L.marker([point.gps_lat, point.gps_lon]);
marker.addTo(map);
markersRef.current.push(marker);
});
if (points.length > 0) {
const group = L.featureGroup(markersRef.current);
map.fitBounds(group.getBounds().pad(0.1));
}
}, [points, map]);
return null;
}
export default function MapPage() {
const [points, setPoints] = useState<GeoPoint[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/geo")
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch geo points");
return res.json();
})
.then((data) => {
setPoints(data);
})
.catch((err) => {
setError(err instanceof Error ? err.message : "Unknown error");
})
.finally(() => {
setLoading(false);
});
}, []);
return (
<main style={{ padding: 16, display: "grid", gap: 16, height: "calc(100vh - 32px)" }}>
<header>
<h1 style={{ marginTop: 0 }}>Map</h1>
</header>
{loading ? (
<div style={{ textAlign: "center", padding: 40 }}>
Loading map...
</div>
) : error ? (
<div style={{ textAlign: "center", padding: 40, color: "#b00" }}>
Error: {error}
</div>
) : points.length === 0 ? (
<div style={{ textAlign: "center", padding: 40, color: "#666" }}>
No GPS points available
</div>
) : (
<div style={{ flex: 1, minHeight: 0 }}>
<MapContainer
style={{ height: "100%", width: "100%" }}
boundsOptions={{ padding: [50, 50] }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapContent points={points} error={error} />
</MapContainer>
</div>
)}
</main>
);
}

View File

@@ -16,6 +16,9 @@ export default function HomePage() {
<header>
<h1 style={{ marginTop: 0 }}>{getAppName()}</h1>
<ul>
<li>
<Link href="/map">Map</Link>
</li>
<li>
<Link href="/admin">Admin</Link>
</li>

View File

@@ -5,9 +5,12 @@
"": {
"name": "tline",
"dependencies": {
"leaflet": "^1.9.4",
"react-leaflet": "^5.0.0",
"zod": "^4.2.1",
},
"devDependencies": {
"@types/leaflet": "^1.9.21",
"@types/node": "^20.19.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
@@ -272,6 +275,8 @@
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw=="],
"@react-leaflet/core": ["@react-leaflet/core@3.0.0", "", { "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ=="],
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw=="],
"@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="],
@@ -390,8 +395,12 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/leaflet": ["@types/leaflet@1.9.21", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w=="],
"@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
@@ -518,6 +527,8 @@
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
@@ -576,6 +587,8 @@
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"react-leaflet": ["react-leaflet@5.0.0", "", { "dependencies": { "@react-leaflet/core": "^3.0.0" }, "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw=="],
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],

View File

@@ -15,6 +15,7 @@
"format": "bunx prettier . --check"
},
"devDependencies": {
"@types/leaflet": "^1.9.21",
"@types/node": "^20.19.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
@@ -24,6 +25,8 @@
"typescript": "^5.9.3"
},
"dependencies": {
"leaflet": "^1.9.4",
"react-leaflet": "^5.0.0",
"zod": "^4.2.1"
}
}