From 4ad5f2314a3b89bf23bddfee0c668a746f0d463c Mon Sep 17 00:00:00 2001 From: Rajtosh Ranjan Date: Sun, 24 May 2026 20:45:47 +0530 Subject: [PATCH] Add basemap selector with thumbnails and toggling Expose activeBasemap state in GlobalContext (default 'satellite'). Add basemap dropdown UI in Header with thumbnails and options. Register raster sources/layers for standard/light/dark and sync layer visibility from MapView when activeBasemap changes. --- src/app.tsx | 3 + src/contexts/global.context.ts | 3 + src/header/header.tsx | 136 ++++++++++++++++++++++++++++++++- src/map-container/constants.ts | 55 +++++++++++++ src/map-container/map-view.tsx | 22 +++++- 5 files changed, 217 insertions(+), 2 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index e5fd743..59aeed1 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -17,6 +17,7 @@ function App() { // States. const [map, setMap] = useState(); const [editingLayerId, setEditingLayerId] = useState(); + const [activeBasemap, setActiveBasemap] = useState('satellite'); // Hooks. const { @@ -36,6 +37,8 @@ function App() { layerManager, editingLayerId, setEditingLayerId, + activeBasemap, + setActiveBasemap, }} >
diff --git a/src/contexts/global.context.ts b/src/contexts/global.context.ts index 380b9c4..098a46e 100644 --- a/src/contexts/global.context.ts +++ b/src/contexts/global.context.ts @@ -11,8 +11,11 @@ type GlobalContextType = { layerManager?: LayerManager; editingLayerId?: string; setEditingLayerId?: React.Dispatch>; + activeBasemap?: string; + setActiveBasemap?: React.Dispatch>; }; export const GlobalContext = createContext({ isLayerLoading: true, + activeBasemap: 'satellite', }); diff --git a/src/header/header.tsx b/src/header/header.tsx index a154e5d..136d43d 100644 --- a/src/header/header.tsx +++ b/src/header/header.tsx @@ -1,11 +1,80 @@ -import { useState } from 'react'; +import { useState, useContext } from 'react'; +import classNames from 'classnames'; import { DropdownMenu, Icon, IconIdentifier } from '../components'; import { AuthPanel } from '../auth-panel'; +import { GlobalContext } from '../contexts'; import { Search } from './search'; export const Header = () => { const [showAuthModal, setShowAuthModal] = useState(false); + const { activeBasemap, setActiveBasemap } = useContext(GlobalContext); + + const basemapOptions = [ + { id: 'standard', name: 'Standard', desc: 'Standard street map' }, + { id: 'satellite', name: 'Satellite', desc: 'Hybrid satellite imagery' }, + { id: 'light', name: 'Light', desc: 'Minimalist light style' }, + { id: 'dark', name: 'Dark', desc: 'Minimalist dark style' }, + { id: 'none', name: 'None', desc: 'Blank slate workspace' }, + ]; + + const renderBasemapThumbnail = (id: string, sizeClass: string) => { + switch (id) { + case 'satellite': + return ( +
+
+
+
+
+ ); + case 'standard': + return ( +
+
+
+
+
+
+ ); + case 'light': + return ( +
+
+
+
+
+ ); + case 'dark': + return ( +
+
+
+
+
+ ); + case 'none': + default: + return ( +
+
+
+ None +
+
+ ); + } + }; return (
@@ -44,6 +113,71 @@ export const Header = () => { {/* Right Side */}
+ {/* Basemap Dropdown */} + + {renderBasemapThumbnail(activeBasemap || 'satellite', 'size-4')} + + {activeBasemap} + +
+ } + className="flex h-8 items-center justify-center rounded-lg border border-transparent bg-transparent px-2 text-gray-400 transition-colors hover:border-gray-700 hover:bg-gray-800 hover:text-gray-50 focus:border-gray-700 focus:bg-gray-800 focus:text-gray-50 data-[open]:border-gray-700 data-[open]:bg-gray-800 data-[open]:text-gray-50" + anchor="bottom end" + > +
+ Basemap +
+ {basemapOptions.map((opt) => { + const isActive = activeBasemap === opt.id; + return ( + setActiveBasemap?.(opt.id)} + className={classNames( + 'flex w-56 items-center gap-3 px-3 py-2 transition-all duration-150', + isActive + ? 'bg-blue-500/10 font-medium text-blue-400' + : 'text-gray-300 hover:text-white', + )} + > +
+ {renderBasemapThumbnail(opt.id, 'size-8')} +
+
+ + {opt.name} + + + {opt.desc} + +
+ {isActive && ( +
+ +
+ )} +
+ ); + })} + + > = ({ const mapContainerRef = useRef(null); // Context. - const { setMap } = useContext(GlobalContext); + const { setMap, map, activeBasemap } = useContext(GlobalContext); const { values: authRecords } = useAuthStore(); const authRecordsRef = useRef(authRecords); @@ -23,6 +23,26 @@ export const MapView: React.FC> = ({ authRecordsRef.current = authRecords; }, [authRecords]); + // Sync basemap layer visibilities + useEffect(() => { + if (!map) return; + + const basemapLayerIds = ['satellite', 'standard', 'light', 'dark']; + basemapLayerIds.forEach((id) => { + try { + if (map.getLayer(id)) { + map.setLayoutProperty( + id, + 'visibility', + activeBasemap === id ? 'visible' : 'none', + ); + } + } catch (err) { + console.warn(`Failed to toggle visibility for layer ${id}:`, err); + } + }); + }, [map, activeBasemap]); + // Constants. const customClassNames = classNames( 'w-full h-screen-container bg-gray-600',