Skip to content

Commit f101484

Browse files
committed
Refactor code structure for improved readability and maintainability
1 parent 3032f98 commit f101484

File tree

3 files changed

+508
-91
lines changed

3 files changed

+508
-91
lines changed

packages/plugin-map/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"@object-ui/types": "workspace:*",
3838
"@objectstack/spec": "^0.9.2",
3939
"lucide-react": "^0.563.0",
40+
"maplibre-gl": "^5.17.0",
41+
"react-map-gl": "^8.1.0",
4042
"zod": "^4.3.6"
4143
},
4244
"peerDependencies": {

packages/plugin-map/src/ObjectMap.tsx

Lines changed: 85 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,16 @@
1616
* Features:
1717
* - Interactive map with markers
1818
* - Location-based data visualization
19-
* - Marker clustering (when many points)
2019
* - Popup/tooltip on marker click
2120
* - Works with object/api/value data providers
22-
*
23-
* Note: This is a basic implementation. For production use, integrate with a
24-
* proper mapping library like Mapbox, Leaflet, or Google Maps.
2521
*/
2622

2723
import React, { useEffect, useState, useMemo } from 'react';
2824
import type { ObjectGridSchema, DataSource, ViewData } from '@object-ui/types';
2925
import { z } from 'zod';
26+
import Map, { NavigationControl, Marker, Popup } from 'react-map-gl/maplibre';
27+
import maplibregl from 'maplibre-gl';
28+
import 'maplibre-gl/dist/maplibre-gl.css';
3029

3130
const MapConfigSchema = z.object({
3231
latitudeField: z.string().optional(),
@@ -222,7 +221,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
222221
const [loading, setLoading] = useState(true);
223222
const [error, setError] = useState<Error | null>(null);
224223
const [objectSchema, setObjectSchema] = useState<any>(null);
225-
const [selectedMarker, setSelectedMarker] = useState<string | null>(null);
224+
const [selectedMarkerId, setSelectedMarkerId] = useState<string | null>(null);
226225

227226
const rawDataConfig = getDataConfig(schema);
228227
// Memoize dataConfig using deep comparison to prevent infinite loops
@@ -336,48 +335,58 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
336335
const title = mapConfig.titleField ? record[mapConfig.titleField] : 'Marker';
337336
const description = mapConfig.descriptionField ? record[mapConfig.descriptionField] : undefined;
338337

338+
// Ensure lat/lng are within valid ranges
339+
const [lat, lng] = coordinates;
340+
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
341+
console.warn(`Invalid coordinates for marker ${index}: [${lat}, ${lng}]`);
342+
return null;
343+
}
344+
339345
return {
340346
id: record.id || record._id || `marker-${index}`,
341347
title,
342348
description,
343-
coordinates,
349+
coordinates: [lng, lat] as [number, number], // maplibre uses [lng, lat]
344350
data: record,
345351
};
346352
})
347353
.filter((marker): marker is NonNullable<typeof marker> => marker !== null);
348354
}, [data, mapConfig]);
349355

356+
const selectedMarker = useMemo(() =>
357+
markers.find(m => m.id === selectedMarkerId),
358+
[markers, selectedMarkerId]);
359+
350360
// Calculate map bounds
351-
const bounds = useMemo(() => {
361+
const initialViewState = useMemo(() => {
352362
if (!markers.length) {
353363
return {
354-
center: mapConfig.center || [0, 0],
355-
minLat: (mapConfig.center?.[0] || 0) - 0.1,
356-
maxLat: (mapConfig.center?.[0] || 0) + 0.1,
357-
minLng: (mapConfig.center?.[1] || 0) - 0.1,
358-
maxLng: (mapConfig.center?.[1] || 0) + 0.1,
364+
longitude: mapConfig.center?.[1] || 0,
365+
latitude: mapConfig.center?.[0] || 0,
366+
zoom: mapConfig.zoom || 2
359367
};
360368
}
361369

362-
const lats = markers.map(m => m.coordinates[0]);
363-
const lngs = markers.map(m => m.coordinates[1]);
370+
// Simple bounds calculation
371+
const lngs = markers.map(m => m.coordinates[0]);
372+
const lats = markers.map(m => m.coordinates[1]);
373+
374+
const minLng = Math.min(...lngs);
375+
const maxLng = Math.max(...lngs);
376+
const minLat = Math.min(...lats);
377+
const maxLat = Math.max(...lats);
364378

365379
return {
366-
center: [
367-
(Math.min(...lats) + Math.max(...lats)) / 2,
368-
(Math.min(...lngs) + Math.max(...lngs)) / 2,
369-
] as [number, number],
370-
minLat: Math.min(...lats),
371-
maxLat: Math.max(...lats),
372-
minLng: Math.min(...lngs),
373-
maxLng: Math.max(...lngs),
380+
longitude: (minLng + maxLng) / 2,
381+
latitude: (minLat + maxLat) / 2,
382+
zoom: mapConfig.zoom || 3, // Auto-zoom logic could be improved here
374383
};
375-
}, [markers, mapConfig.center]);
384+
}, [markers, mapConfig]);
376385

377386
if (loading) {
378387
return (
379388
<div className={className}>
380-
<div className="flex items-center justify-center h-96 bg-muted rounded-lg">
389+
<div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
381390
<div className="text-muted-foreground">Loading map...</div>
382391
</div>
383392
</div>
@@ -387,7 +396,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
387396
if (error) {
388397
return (
389398
<div className={className}>
390-
<div className="flex items-center justify-center h-96 bg-muted rounded-lg">
399+
<div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
391400
<div className="text-destructive">Error: {error.message}</div>
392401
</div>
393402
</div>
@@ -396,75 +405,60 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
396405

397406
return (
398407
<div className={className}>
399-
<div className="relative border rounded-lg overflow-hidden bg-muted" style={{ height: '600px' }}>
400-
{/* Placeholder map - in production, replace with actual map library */}
401-
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-green-50 dark:from-blue-950 dark:to-green-950">
402-
<div className="text-center">
403-
<div className="text-4xl mb-2">🗺️</div>
404-
<div className="text-sm text-muted-foreground mb-4">
405-
Map Visualization (Placeholder)
406-
</div>
407-
<div className="text-xs text-muted-foreground max-w-md mx-auto">
408-
This is a basic map placeholder. For production, integrate with Mapbox, Leaflet, or Google Maps.
409-
<br />
410-
<br />
411-
<strong>Map Info:</strong>
412-
<br />
413-
Center: [{bounds.center[0].toFixed(4)}, {bounds.center[1].toFixed(4)}]
414-
<br />
415-
Markers: {markers.length}
416-
</div>
417-
</div>
418-
</div>
419-
420-
{/* Marker List Overlay */}
421-
<div className="absolute top-4 right-4 w-64 bg-background border rounded-lg shadow-lg max-h-96 overflow-y-auto">
422-
<div className="p-3 border-b font-semibold bg-muted">
423-
Locations ({markers.length})
424-
</div>
425-
{markers.length === 0 ? (
426-
<div className="p-4 text-sm text-muted-foreground text-center">
427-
No locations found with valid coordinates
428-
</div>
429-
) : (
430-
<div>
431-
{markers.map(marker => (
432-
<div
433-
key={marker.id}
434-
className={`p-3 border-b hover:bg-muted/50 cursor-pointer transition-colors ${
435-
selectedMarker === marker.id ? 'bg-muted' : ''
436-
}`}
437-
onClick={() => {
438-
setSelectedMarker(marker.id);
439-
onMarkerClick?.(marker.data);
440-
}}
408+
<div className="relative border rounded-lg overflow-hidden bg-muted" style={{ height: '600px', width: '100%' }}>
409+
<Map
410+
initialViewState={initialViewState}
411+
style={{ width: '100%', height: '100%' }}
412+
mapStyle="https://demotiles.maplibre.org/style.json"
413+
>
414+
<NavigationControl position="top-right" />
415+
416+
{markers.map(marker => (
417+
<Marker
418+
key={marker.id}
419+
longitude={marker.coordinates[0]}
420+
latitude={marker.coordinates[1]}
421+
anchor="bottom"
422+
onClick={(e) => {
423+
e.originalEvent.stopPropagation();
424+
setSelectedMarkerId(marker.id);
425+
onMarkerClick?.(marker.data);
426+
}}
441427
>
442-
<div className="font-medium text-sm">{marker.title}</div>
443-
{marker.description && (
444-
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">
445-
{marker.description}
428+
<div className="text-2xl cursor-pointer hover:scale-110 transition-transform">
429+
📍
446430
</div>
447-
)}
448-
<div className="text-xs text-muted-foreground mt-1">
449-
📍 {marker.coordinates[0].toFixed(4)}, {marker.coordinates[1].toFixed(4)}
450-
</div>
451-
</div>
452-
))}
453-
</div>
454-
)}
455-
</div>
456-
457-
{/* Legend */}
458-
<div className="absolute bottom-4 left-4 bg-background border rounded-lg shadow-lg p-3">
459-
<div className="text-xs font-semibold mb-2">Legend</div>
460-
<div className="text-xs space-y-1">
461-
<div className="flex items-center gap-2">
462-
<div className="w-3 h-3 rounded-full bg-blue-500" />
463-
<span>Location Marker</span>
464-
</div>
465-
</div>
466-
</div>
431+
</Marker>
432+
))}
433+
434+
{selectedMarker && (
435+
<Popup
436+
longitude={selectedMarker.coordinates[0]}
437+
latitude={selectedMarker.coordinates[1]}
438+
anchor="top"
439+
onClose={() => setSelectedMarkerId(null)}
440+
closeOnClick={false}
441+
>
442+
<div className="p-2 min-w-[200px]">
443+
<h3 className="font-bold text-sm mb-1">{selectedMarker.title}</h3>
444+
{selectedMarker.description && (
445+
<p className="text-xs text-muted-foreground">{selectedMarker.description}</p>
446+
)}
447+
<div className="mt-2 text-xs flex gap-2">
448+
{onEdit && (
449+
<button className="text-blue-500 hover:underline" onClick={() => onEdit(selectedMarker.data)}>Edit</button>
450+
)}
451+
{onDelete && (
452+
<button className="text-red-500 hover:underline" onClick={() => onDelete(selectedMarker.data)}>Delete</button>
453+
)}
454+
</div>
455+
</div>
456+
</Popup>
457+
)}
458+
</Map>
467459
</div>
468460
</div>
469461
);
470462
};
463+
464+
export default ObjectMap;

0 commit comments

Comments
 (0)