diff --git a/web/src/app/components/MapNavigationControl.tsx b/web/src/app/components/MapNavigationControl.tsx new file mode 100644 index 0000000..2b2badb --- /dev/null +++ b/web/src/app/components/MapNavigationControl.tsx @@ -0,0 +1,32 @@ +"use client"; + +import React, { useEffect } from 'react'; +import mapboxgl from 'mapbox-gl'; + +type Position = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; + +interface Props { + mapRef: React.MutableRefObject; + position?: Position; + showCompass?: boolean; + showZoom?: boolean; + visualizePitch?: boolean; + style?: React.CSSProperties; +} + +export default function MapNavigationControl({ mapRef, position = 'top-right', showCompass = true, showZoom = true, visualizePitch = false, style }: Props) { + useEffect(() => { + const map = mapRef.current; + if (!map) return; + + const nav = new mapboxgl.NavigationControl({ showCompass, showZoom, visualizePitch }); + map.addControl(nav, position); + + return () => { + try { map.removeControl(nav); } catch (e) {} + }; + }, [mapRef, position, showCompass, showZoom, visualizePitch]); + + // the control is rendered by mapbox, so this component itself renders nothing + return null; +} diff --git a/web/src/app/components/MapView.tsx b/web/src/app/components/MapView.tsx index 3eb6f20..ccf9b1c 100644 --- a/web/src/app/components/MapView.tsx +++ b/web/src/app/components/MapView.tsx @@ -41,11 +41,27 @@ export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, hea return () => ro.disconnect(); }, []); + // react to style choice changes + useEffect(() => { + const map = mapRef.current; + if (!map) return; + const styleUrl = mapStyleChoice === 'dark' ? 'mapbox://styles/mapbox/dark-v10' : 'mapbox://styles/mapbox/streets-v11'; + try { + map.setStyle(styleUrl); + } catch (e) { + // some map versions may throw; still listen for styledata to re-add layers + } + }, [mapStyleChoice]); + useEffect(() => { const mapEl = mapContainerRef.current; if (!mapEl) return; - mapboxgl.accessToken = 'pk.eyJ1IjoicGllbG9yZDc1NyIsImEiOiJjbWcxdTd6c3AwMXU1MmtxMDh6b2l5amVrIn0.5Es0azrah23GX1e9tmbjGw'; + const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || process.env.NEXT_PUBLIC_MAPBOX_TOKEN; + if (!token) { + console.warn('Missing NEXT_PUBLIC_MAPBOX_TOKEN environment variable. Mapbox map will not initialize correctly.'); + } + mapboxgl.accessToken = token ?? ''; const styleUrl = mapStyleChoice === 'dark' ? 'mapbox://styles/mapbox/dark-v10' diff --git a/web/src/app/components/ZoomControls.tsx b/web/src/app/components/ZoomControls.tsx index de9e5df..67f1526 100644 --- a/web/src/app/components/ZoomControls.tsx +++ b/web/src/app/components/ZoomControls.tsx @@ -1,15 +1,15 @@ "use client"; -import React from 'react'; -import mapboxgl from 'mapbox-gl'; +// import React from 'react'; +// import mapboxgl from 'mapbox-gl'; -interface Props { mapRef: React.MutableRefObject } +// interface Props { mapRef: React.MutableRefObject } -export default function ZoomControls({ mapRef }: Props) { - return ( -
- - -
- ); -} +// export default function ZoomControls({ mapRef }: Props) { +// return ( +//
+// +// +//
+// ); +// } diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index f476bd9..a53bd9a 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -3,32 +3,50 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: "--font-geist-sans", + subsets: ["latin"], }); const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: "--font-geist-mono", + subsets: ["latin"], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "DC Heatmap Explorer — Washington, D.C. Density Visualizer", + description: + "Interactive heatmap of simulated points across Washington, D.C. Toggle heatmap, inspect nearby statistics, and explore data-driven areas.", + openGraph: { + title: "DC Heatmap Explorer — Washington, D.C.", + description: + "Interactive heatmap of simulated points across Washington, D.C.", + url: "https://your-domain.example/", + siteName: "DC Heatmap Explorer", + images: [ + { + url: "https://your-domain.example/og-image.png", + width: 1200, + height: 630, + alt: "DC Heatmap Explorer preview", + }, + ], + locale: "en_US", + type: "website", + } }; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); } diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index e8f752e..da15197 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -5,7 +5,7 @@ import MapView, { PopupData } from './components/MapView'; import ControlsPanel from './components/ControlsPanel'; import PopupOverlay from './components/PopupOverlay'; import Legend from './components/Legend'; -import ZoomControls from './components/ZoomControls'; +import MapNavigationControl from './components/MapNavigationControl'; export default function Home() { const mapRef = useRef(null); @@ -47,8 +47,10 @@ export default function Home() { onPopupCreate={(p) => { setPopupVisible(false); setPopup(p); requestAnimationFrame(() => setPopupVisible(true)); }} /> + {/* Native Mapbox navigation control (zoom + compass) */} + + - { setPopupVisible(false); setTimeout(() => setPopup(null), 220); }} /> );