feat: add map page
This commit is contained in:
96
apps/web/app/map/page.tsx
Normal file
96
apps/web/app/map/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user