aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/components/Map.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/components/Map.tsx')
-rw-r--r--app/src/components/Map.tsx273
1 files changed, 273 insertions, 0 deletions
diff --git a/app/src/components/Map.tsx b/app/src/components/Map.tsx
new file mode 100644
index 0000000..c6e9583
--- /dev/null
+++ b/app/src/components/Map.tsx
@@ -0,0 +1,273 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { MapContainer, TileLayer, Marker, Popup, useMap, Polyline } from "react-leaflet";
+import L from "leaflet";
+import "leaflet/dist/leaflet.css";
+import { toast } from "sonner";
+import { useSocket } from "@/hooks/useSocket";
+
+// Type for location
+interface LocationType {
+ latitude: number;
+ longitude: number;
+ accuracy?: number;
+ timestamp: Date;
+}
+
+// Location update handler that recalculates position
+function LocationMarker({
+ position,
+ onLocationUpdate,
+ shareToken,
+}: {
+ position: [number, number] | null;
+ onLocationUpdate: (location: LocationType) => void;
+ shareToken?: string;
+}) {
+ const [positionHistory, setPositionHistory] = useState<[number, number][]>([]);
+ const [isClient, setIsClient] = useState(false);
+ const map = useMap();
+ const { sendLocationUpdate, isConnected } = useSocket();
+
+ // Safely check if we're on the client side
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ useEffect(() => {
+ map.locate({ watch: true, enableHighAccuracy: true });
+
+ map.on("locationfound", (e) => {
+ const newPosition: [number, number] = [e.latlng.lat, e.latlng.lng];
+
+ // Update position history
+ setPositionHistory((prev) => [...prev, newPosition]);
+
+ // Create location data
+ const locationData = {
+ latitude: e.latlng.lat,
+ longitude: e.latlng.lng,
+ accuracy: e.accuracy,
+ timestamp: new Date(),
+ };
+
+ // Notify parent component
+ onLocationUpdate(locationData);
+
+ // Send location update to Socket.IO if sharing
+ if (shareToken && isConnected) {
+ sendLocationUpdate({
+ latitude: e.latlng.lat,
+ longitude: e.latlng.lng,
+ accuracy: e.accuracy,
+ shareToken,
+ });
+ }
+
+ // Center map on the new position
+ map.flyTo(e.latlng, map.getZoom());
+ });
+
+ map.on("locationerror", (e) => {
+ toast.error("Error accessing location: " + e.message);
+ console.error("Location error: ", e);
+ });
+
+ return () => {
+ map.stopLocate();
+ map.off("locationfound");
+ map.off("locationerror");
+ };
+ }, [map, onLocationUpdate, sendLocationUpdate, shareToken, isConnected]);
+
+ // Return null if position is null or if we're not on client yet
+ if (!position || !isClient) return null;
+
+ // Only render the polyline if we have enough points and we're on the client side
+ const showPolyline = isClient && positionHistory && positionHistory.length > 1;
+
+ return (
+ <>
+ <Marker position={position}>
+ <Popup>
+ {shareToken ? "Sharing location in real-time" : "You are here"}
+ {isConnected && shareToken && (
+ <div className="text-xs mt-1 text-green-600">Connected</div>
+ )}
+ </Popup>
+ </Marker>
+
+ {/* Draw path if we have position history and we're on client */}
+ {showPolyline && (
+ <Polyline
+ pathOptions={{ color: "blue", weight: 3 }}
+ positions={positionHistory}
+ />
+ )}
+ </>
+ );
+}
+
+// Component to display a shared location
+function SharedLocationMarker({ position, lastUpdate }: { position: [number, number]; lastUpdate?: string }) {
+ return (
+ <Marker position={position} icon={new L.Icon({
+ iconUrl: '/marker-icon-red.png',
+ iconRetinaUrl: '/marker-icon-2x-red.png',
+ shadowUrl: '/marker-shadow.png',
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+ popupAnchor: [1, -34],
+ shadowSize: [41, 41]
+ })}>
+ <Popup>
+ <div>
+ <div>Shared Location</div>
+ {lastUpdate && (
+ <div className="text-xs mt-1">Last updated: {new Date(lastUpdate).toLocaleTimeString()}</div>
+ )}
+ </div>
+ </Popup>
+ </Marker>
+ );
+}
+
+export default function Map({
+ onLocationUpdate,
+ shareToken,
+ mode = 'tracking',
+ initialLocation,
+}: {
+ onLocationUpdate?: (location: LocationType) => void;
+ shareToken?: string;
+ mode?: 'tracking' | 'viewing';
+ initialLocation?: { latitude: number; longitude: number };
+}) {
+ const [position, setPosition] = useState<[number, number] | null>(null);
+ const [sharedPosition, setSharedPosition] = useState<[number, number] | null>(null);
+ const [lastUpdate, setLastUpdate] = useState<string | undefined>(undefined);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isClient, setIsClient] = useState(false);
+ const { subscribeToLocationUpdates } = useSocket();
+
+ // Check if we're on client side
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ // Fix Leaflet marker icon issues in Next.js
+ useEffect(() => {
+ // This is needed to fix the marker icon issues with webpack
+ if (typeof window !== "undefined") {
+ // @ts-ignore
+ delete L.Icon.Default.prototype._getIconUrl;
+ L.Icon.Default.mergeOptions({
+ iconRetinaUrl: "/marker-icon-2x.png",
+ iconUrl: "/marker-icon.png",
+ shadowUrl: "/marker-shadow.png",
+ });
+ }
+ }, []);
+
+ // Subscribe to real-time location updates when viewing shared location
+ useEffect(() => {
+ if (mode === 'viewing' && shareToken && isClient) {
+ // If we have initial location, set it
+ if (initialLocation) {
+ setSharedPosition([initialLocation.latitude, initialLocation.longitude]);
+ setIsLoading(false);
+ }
+
+ // Subscribe to real-time updates
+ const cleanup = subscribeToLocationUpdates(shareToken, (data) => {
+ console.log('Received location update:', data);
+ setSharedPosition([data.latitude, data.longitude]);
+ setLastUpdate(data.timestamp);
+ toast.info('Location updated');
+ });
+
+ return cleanup;
+ }
+ }, [mode, shareToken, subscribeToLocationUpdates, initialLocation, isClient]);
+
+ useEffect(() => {
+ if (!isClient) return;
+
+ // Only get current position in tracking mode
+ if (mode === 'tracking') {
+ // Try to get initial position
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ setPosition([position.coords.latitude, position.coords.longitude]);
+ setIsLoading(false);
+
+ if (onLocationUpdate) {
+ onLocationUpdate({
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude,
+ accuracy: position.coords.accuracy,
+ timestamp: new Date(position.timestamp),
+ });
+ }
+ },
+ (error) => {
+ toast.error(`Error getting location: ${error.message}`);
+ setIsLoading(false);
+ },
+ { enableHighAccuracy: true }
+ );
+ }
+ }, [onLocationUpdate, isClient, mode]);
+
+ // Handle location updates from the marker component
+ const handleLocationUpdate = (location: LocationType) => {
+ setPosition([location.latitude, location.longitude]);
+ if (onLocationUpdate) {
+ onLocationUpdate(location);
+ }
+ };
+
+ if (!isClient || isLoading) {
+ return <div className="h-full w-full flex items-center justify-center">Loading map...</div>;
+ }
+
+ // Determine which position to center on
+ let defaultPosition: [number, number];
+ if (mode === 'viewing' && sharedPosition) {
+ defaultPosition = sharedPosition;
+ } else if (position) {
+ defaultPosition = position;
+ } else {
+ defaultPosition = [51.505, -0.09]; // Default to London
+ }
+
+ return (
+ <div className="h-full w-full">
+ <MapContainer
+ center={defaultPosition}
+ zoom={13}
+ style={{ height: "100%", width: "100%" }}
+ >
+ <TileLayer
+ attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
+ url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
+ />
+
+ {/* Show our location marker in tracking mode */}
+ {mode === 'tracking' && position && (
+ <LocationMarker
+ position={position}
+ onLocationUpdate={handleLocationUpdate}
+ shareToken={shareToken}
+ />
+ )}
+
+ {/* Show shared location marker in viewing mode */}
+ {mode === 'viewing' && sharedPosition && (
+ <SharedLocationMarker position={sharedPosition} lastUpdate={lastUpdate} />
+ )}
+ </MapContainer>
+ </div>
+ );
+} \ No newline at end of file