132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { MapContainer, TileLayer, Marker, Popup, CircleMarker } from 'react-leaflet';
|
|
import * as turf from '@turf/turf';
|
|
import 'leaflet/dist/leaflet.css';
|
|
|
|
/**
|
|
* Campaign Map Component
|
|
* Visualize geospatial content coverage
|
|
*/
|
|
|
|
interface Location {
|
|
id: string;
|
|
lat: number;
|
|
lng: number;
|
|
city?: string;
|
|
state?: string;
|
|
content_generated?: boolean;
|
|
}
|
|
|
|
interface CampaignMapProps {
|
|
geoData?: {
|
|
type: string;
|
|
features: any[];
|
|
};
|
|
defaultCenter?: [number, number];
|
|
defaultZoom?: number;
|
|
}
|
|
|
|
export default function CampaignMap({
|
|
geoData,
|
|
defaultCenter = [39.8283, -98.5795], // Center of USA
|
|
defaultZoom = 4
|
|
}: CampaignMapProps) {
|
|
const [locations, setLocations] = useState<Location[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetchLocations();
|
|
}, []);
|
|
|
|
async function fetchLocations() {
|
|
try {
|
|
const token = localStorage.getItem('godToken') || '';
|
|
const res = await fetch('/api/god/sql', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-God-Token': token
|
|
},
|
|
body: JSON.stringify({
|
|
query: `
|
|
SELECT
|
|
id::text,
|
|
ST_Y(location::geometry) as lat,
|
|
ST_X(location::geometry) as lng,
|
|
city,
|
|
state,
|
|
content_generated
|
|
FROM geo_locations
|
|
WHERE location IS NOT NULL
|
|
LIMIT 1000
|
|
`
|
|
})
|
|
});
|
|
|
|
if (!res.ok) {
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
setLocations(data.rows || []);
|
|
setIsLoading(false);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch locations:', error);
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="w-full h-[500px] bg-slate-900 rounded-xl flex items-center justify-center text-slate-500">
|
|
Loading map data...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full h-[500px] rounded-xl overflow-hidden border border-slate-800">
|
|
<MapContainer
|
|
center={defaultCenter}
|
|
zoom={defaultZoom}
|
|
style={{ height: '100%', width: '100%' }}
|
|
className="z-0"
|
|
>
|
|
{/* Tile Layer (Map Background) */}
|
|
<TileLayer
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
|
/>
|
|
|
|
{/* Location Markers */}
|
|
{locations.map((location) => (
|
|
<CircleMarker
|
|
key={location.id}
|
|
center={[location.lat, location.lng]}
|
|
radius={location.content_generated ? 8 : 5}
|
|
fillColor={location.content_generated ? '#10b981' : '#3b82f6'}
|
|
color={location.content_generated ? '#059669' : '#2563eb'}
|
|
weight={1}
|
|
opacity={0.8}
|
|
fillOpacity={0.6}
|
|
>
|
|
<Popup>
|
|
<div className="text-sm">
|
|
<strong>{location.city || 'Unknown'}, {location.state || '??'}</strong>
|
|
<br />
|
|
Status: {location.content_generated ? '✅ Generated' : '⏳ Pending'}
|
|
<br />
|
|
<span className="text-xs text-gray-500">
|
|
{location.lat.toFixed(4)}, {location.lng.toFixed(4)}
|
|
</span>
|
|
</div>
|
|
</Popup>
|
|
</CircleMarker>
|
|
))}
|
|
</MapContainer>
|
|
</div>
|
|
);
|
|
}
|