Add interactive Leaflet burrow map + fix stream API field mapping
This commit is contained in:
parent
857f5a2396
commit
581ffde7fc
1
.npmrc
1
.npmrc
@ -1,3 +1,4 @@
|
|||||||
# npm configuration
|
# npm configuration
|
||||||
registry=https://registry.npmjs.org/
|
registry=https://registry.npmjs.org/
|
||||||
save-exact=true
|
save-exact=true
|
||||||
|
legacy-peer-deps=true
|
||||||
|
|||||||
84
app/map/BurrowMap.tsx
Normal file
84
app/map/BurrowMap.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { MapContainer, TileLayer, Marker, Popup, CircleMarker } from 'react-leaflet';
|
||||||
|
import L from 'leaflet';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
|
||||||
|
interface Burrow {
|
||||||
|
id: string;
|
||||||
|
gps_lat: number;
|
||||||
|
gps_lng: number;
|
||||||
|
status: string;
|
||||||
|
location_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix default marker icons in Next.js
|
||||||
|
const activeIcon = new L.DivIcon({
|
||||||
|
className: '',
|
||||||
|
html: '<div style="width:14px;height:14px;border-radius:50%;background:#0082a7;box-shadow:0 0 10px rgba(0,130,167,0.8);border:2px solid rgba(255,255,255,0.3)"></div>',
|
||||||
|
iconSize: [14, 14],
|
||||||
|
iconAnchor: [7, 7],
|
||||||
|
});
|
||||||
|
|
||||||
|
const inactiveIcon = new L.DivIcon({
|
||||||
|
className: '',
|
||||||
|
html: '<div style="width:12px;height:12px;border-radius:50%;background:#57534e;border:2px solid rgba(255,255,255,0.15)"></div>',
|
||||||
|
iconSize: [12, 12],
|
||||||
|
iconAnchor: [6, 6],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function BurrowMap() {
|
||||||
|
const [burrows, setBurrows] = useState<Burrow[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||||
|
fetch(`${apiUrl}/api/burrows`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => { setBurrows(data); setLoading(false); })
|
||||||
|
.catch(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-stone-500">
|
||||||
|
<div className="animate-spin w-8 h-8 border-2 border-teal border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cape Coral center
|
||||||
|
const center: [number, number] = [26.6029, -81.9595];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MapContainer
|
||||||
|
center={center}
|
||||||
|
zoom={13}
|
||||||
|
className="w-full h-full rounded-3xl"
|
||||||
|
style={{ background: '#0a1a15' }}
|
||||||
|
attributionControl={false}
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
|
||||||
|
attribution='© <a href="https://carto.com/">CARTO</a>'
|
||||||
|
/>
|
||||||
|
{burrows.map((b) => (
|
||||||
|
<Marker
|
||||||
|
key={b.id}
|
||||||
|
position={[b.gps_lat, b.gps_lng]}
|
||||||
|
icon={b.status === 'active' ? activeIcon : inactiveIcon}
|
||||||
|
>
|
||||||
|
<Popup>
|
||||||
|
<div className="text-sm space-y-1" style={{ color: '#1a1a1a' }}>
|
||||||
|
<div className="font-bold">{b.status === 'active' ? '🟢 Active' : '⚫ Inactive'} Burrow</div>
|
||||||
|
<div>{b.location_description}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{b.gps_lat.toFixed(4)}, {b.gps_lng.toFixed(4)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
</Marker>
|
||||||
|
))}
|
||||||
|
</MapContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
import { Map } from 'lucide-react';
|
import { Map } from 'lucide-react';
|
||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
@ -8,7 +9,14 @@ export const metadata: Metadata = {
|
|||||||
description: 'Interactive map of burrowing owl burrows in Cape Coral, Florida.',
|
description: 'Interactive map of burrowing owl burrows in Cape Coral, Florida.',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BurrowMap = dynamic(() => import('./BurrowMap'), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-stone-500">
|
||||||
|
<div className="animate-spin w-8 h-8 border-2 border-teal border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
export default function MapPage() {
|
export default function MapPage() {
|
||||||
return (
|
return (
|
||||||
@ -23,27 +31,9 @@ export default function MapPage() {
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Map placeholder — interactive Leaflet map requires client component */}
|
{/* Interactive Leaflet Map */}
|
||||||
<div className="relative rounded-3xl overflow-hidden bg-surfaceGreen border border-white/5 shadow-2xl" style={{ height: 600 }}>
|
<div className="relative rounded-3xl overflow-hidden bg-surfaceGreen border border-white/5 shadow-2xl" style={{ height: 600 }}>
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-6 text-stone-500">
|
<BurrowMap />
|
||||||
<div className="text-8xl">🗺️</div>
|
|
||||||
<div className="text-center space-y-2">
|
|
||||||
<p className="text-xl font-bold text-stone-300">Interactive Burrow Map</p>
|
|
||||||
<p className="text-sm">
|
|
||||||
Leaflet map with burrow markers loading from{' '}
|
|
||||||
<code className="text-teal text-xs bg-black/30 px-2 py-0.5 rounded">/api/burrows</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Decorative grid pattern */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 opacity-5"
|
|
||||||
style={{
|
|
||||||
backgroundImage:
|
|
||||||
'linear-gradient(rgba(0,130,167,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(0,130,167,0.5) 1px, transparent 1px)',
|
|
||||||
backgroundSize: '40px 40px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
|
|||||||
50
package-lock.json
generated
50
package-lock.json
generated
@ -8,14 +8,18 @@
|
|||||||
"name": "owl-stream-frontend",
|
"name": "owl-stream-frontend",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-leaflet/core": "2.1.0",
|
||||||
|
"@types/leaflet": "1.9.21",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"hls.js": "^1.5.7",
|
"hls.js": "^1.5.7",
|
||||||
|
"leaflet": "1.9.4",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"next": "14.1.3",
|
"next": "14.1.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-leaflet": "4.2.1",
|
||||||
"tailwind-merge": "^2.2.1"
|
"tailwind-merge": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -529,6 +533,17 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-leaflet/core": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
|
||||||
|
"license": "Hippocratic-2.1",
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@ -552,6 +567,12 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json5": {
|
"node_modules/@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
@ -559,6 +580,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/leaflet": {
|
||||||
|
"version": "1.9.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
|
||||||
|
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.16.13",
|
"version": "20.16.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz",
|
||||||
@ -3463,6 +3493,12 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@ -4354,6 +4390,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-leaflet": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
|
||||||
|
"license": "Hippocratic-2.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-leaflet/core": "^2.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
|||||||
@ -9,14 +9,18 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-leaflet/core": "2.1.0",
|
||||||
|
"@types/leaflet": "1.9.21",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"hls.js": "^1.5.7",
|
"hls.js": "^1.5.7",
|
||||||
|
"leaflet": "1.9.4",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"next": "14.1.3",
|
"next": "14.1.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-leaflet": "4.2.1",
|
||||||
"tailwind-merge": "^2.2.1"
|
"tailwind-merge": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user