diff --git a/frontend/src/app/Store.ts b/frontend/src/app/Store.ts index 5b77314b..593ef4ae 100644 --- a/frontend/src/app/Store.ts +++ b/frontend/src/app/Store.ts @@ -17,6 +17,7 @@ import associatedSitesReducer from './features/details/associates/AssociateSlice import parcelDescriptionsReducer from './features/details/parcelDescriptions/parcelDescriptionsSlice'; import srUpdatesReducer from './features/details/srUpdates/srUpdatesSlice'; import srReviewReducer from './features/details/srUpdates/state/srUpdatesTableSlice'; +import mapReducer from './features/map/map-slice'; const persistedStore: any = loadFromLocalStorage(); @@ -39,6 +40,7 @@ export const store = configureStore({ parcelDescriptions: parcelDescriptionsReducer, srUpdates: srUpdatesReducer, srReview: srReviewReducer, + map: mapReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/frontend/src/app/features/map/Control.tsx b/frontend/src/app/features/map/Control.tsx new file mode 100644 index 00000000..2f5568b3 --- /dev/null +++ b/frontend/src/app/features/map/Control.tsx @@ -0,0 +1,80 @@ +import { CSSProperties, ReactNode, useEffect, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { DomUtil } from 'leaflet'; +import clsx from 'clsx'; + +// Allow custom corners like "bottomcenter", "leftmiddle" etc +const POSITION_CLASSES: Record = { + bottomleft: 'leaflet-bottom leaflet-left', + bottomright: 'leaflet-bottom leaflet-right', + topleft: 'leaflet-top leaflet-left', + topright: 'leaflet-top leaflet-right', + bottomcenter: 'leaflet-bottom leaflet-center', + topcenter: 'leaflet-top leaflet-center', + leftmiddle: 'leaflet-left leaflet-middle', + rightmiddle: 'leaflet-right leaflet-middle', +}; + +interface Props { + position: string; + children: ReactNode; + className?: string; + style?: CSSProperties; +} + +export function Control({ position, children, className, style }: Props) { + const [container, setContainer] = useState( + document.createElement('div'), + ); + + useEffect(() => { + // @ts-ignore + let positionClass = POSITION_CLASSES.topright; + if (position && position in POSITION_CLASSES) { + positionClass = POSITION_CLASSES[position]; + } + let targetDiv = document.getElementsByClassName( + positionClass, + )[0] as HTMLElement; + if (!targetDiv) { + // Create custom position + const topRight = document.getElementsByClassName( + POSITION_CLASSES.topright, + )[0]; + targetDiv = DomUtil.create( + 'div', + positionClass, + topRight.parentNode as HTMLElement, + ); + } + setContainer(targetDiv); + }, [position]); + + // Make the attribution the last child (after map controls) + useEffect(() => { + const len = container.children.length; + if (len > 1) { + const attribution = container.querySelector( + '.leaflet-control-attribution', + ); + if (attribution && attribution !== container.children[len - 1]) { + container.removeChild(attribution); + container.appendChild(attribution); + } + } + }, [container, position]); + + const controlContainer = useMemo( + () => ( +
+ {children} +
+ ), + [children, style], + ); + + return createPortal(controlContainer, container); +} diff --git a/frontend/src/app/features/map/FindMeButton.tsx b/frontend/src/app/features/map/FindMeButton.tsx index e0c04b49..8aaac0ed 100644 --- a/frontend/src/app/features/map/FindMeButton.tsx +++ b/frontend/src/app/features/map/FindMeButton.tsx @@ -1,28 +1,23 @@ import { useDispatch } from 'react-redux'; -import { Button } from '@mui/material'; +import { Button, Icon, useMediaQuery, useTheme } from '@mui/material'; import clsx from 'clsx'; -// import { -// setMyLocationVisible, -// useMyLocationVisible, -// } from '@/features/map/map-slice' -// import { useGeolocationPermission } from '@/hooks/useMyLocation' - import { FindMe } from '../../components/common/icon'; +import { useGeolocationPermission } from '../../../hooks/useMyLocation'; +import { setMyLocationVisible, useMyLocationVisible } from './map-slice'; export function FindMeButton() { - // const dispatch = useDispatch() - // const isMarkerVisible = useMyLocationVisible() + const dispatch = useDispatch(); + const isMarkerVisible = useMyLocationVisible(); + const state = useGeolocationPermission(); - // const state = useGeolocationPermission() - // No point in showing the button if the permission has been denied - // if (state === 'denied') { - // return null - // } + if (state === 'denied') { + return null; + } - // const onClick = () => { - // dispatch(setMyLocationVisible(!isMarkerVisible)) - // } + const onClick = () => { + dispatch(setMyLocationVisible(!isMarkerVisible)); + }; return ( diff --git a/frontend/src/app/features/map/FindMeControl.tsx b/frontend/src/app/features/map/FindMeControl.tsx new file mode 100644 index 00000000..ccfa597e --- /dev/null +++ b/frontend/src/app/features/map/FindMeControl.tsx @@ -0,0 +1,35 @@ +import { useDispatch } from 'react-redux'; +import { IconButton } from '@mui/material'; +import clsx from 'clsx'; + +import { setMyLocationVisible, useMyLocationVisible } from './map-slice'; +import { useGeolocationPermission } from '../../../hooks/useMyLocation'; + +import { FindMe } from '../../components/common/icon'; + +export function FindMeControl() { + const dispatch = useDispatch(); + const isMarkerVisible = useMyLocationVisible(); + const state = useGeolocationPermission(); + // No point in showing the button if the permission has been denied + if (state === 'denied') { + return null; + } + + const onClick = () => { + dispatch(setMyLocationVisible(!isMarkerVisible)); + }; + + return ( + + + + ); +} diff --git a/frontend/src/app/features/map/MapControl.css b/frontend/src/app/features/map/MapControl.css new file mode 100644 index 00000000..e1ade612 --- /dev/null +++ b/frontend/src/app/features/map/MapControl.css @@ -0,0 +1,42 @@ +.map-controls { + display: flex; + flex-direction: column; + gap: 8px; + border: 0 !important; + border-radius: 0; + max-height: calc(100vh - 80px); + transition: margin ease-in-out 0.5s; + position: relative; +} + +.map-controls button.map-control-button { + width: 42px; + height: 42px; + padding: 0; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + border-radius: 6px; + background: var(--surface-color-background-white); + color: var(--icons-color-secondary); + box-shadow: var(--box-shadow-small); + transition: all ease-in-out 0.3s; +} + +.map-controls button.map-control-button:hover { + color: var(--icons-color-info); +} + +.map-controls button.map-control-button:focus { + outline: 2px solid rgba(46, 93, 215, 0.5); +} + +.map-controls button.map-control-button--active { + background-color: var(--surface-color-primary-active-border); + color: var(--surface-color-background-white); +} + +.map-controls button.map-control-button--active:hover { + color: var(--surface-color-secondary-hover); +} diff --git a/frontend/src/app/features/map/MapControls.tsx b/frontend/src/app/features/map/MapControls.tsx new file mode 100644 index 00000000..e4d7a7e2 --- /dev/null +++ b/frontend/src/app/features/map/MapControls.tsx @@ -0,0 +1,49 @@ +import { useMemo } from 'react'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; + +import { + MAP_CONTROLS_BOTTOM_LG, + MAP_CONTROLS_BOTTOM_SM, + MAP_CONTROLS_RIGHT_LG, + MAP_CONTROLS_RIGHT_SM, + MAP_CONTROLS_RIGHT_XL, +} from '../../constants/Constant'; + +import { Control } from './Control'; + +import './MapControl.css'; +import { FindMeControl } from './FindMeControl'; + +export function MapControls() { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down('md')); + const isMedium = useMediaQuery(theme.breakpoints.down('lg')); + const isLarge = useMediaQuery(theme.breakpoints.down('xl')); + + // Shift the controls based on screen size + let right = MAP_CONTROLS_RIGHT_XL; + if (isSmall) { + right = MAP_CONTROLS_RIGHT_SM; + } else if (isLarge) { + right = MAP_CONTROLS_RIGHT_LG; + } + let bottom = MAP_CONTROLS_BOTTOM_LG; + if (isSmall) { + bottom = MAP_CONTROLS_BOTTOM_SM; + } + + const style = useMemo( + () => ({ + marginRight: `${right}px`, + marginBottom: `${bottom}px`, + }), + [right, bottom], + ); + + return ( + + {isMedium && } + + ); +} diff --git a/frontend/src/app/features/map/MapSearch.css b/frontend/src/app/features/map/MapSearch.css index 184ce0f3..ccda91f3 100644 --- a/frontend/src/app/features/map/MapSearch.css +++ b/frontend/src/app/features/map/MapSearch.css @@ -88,7 +88,8 @@ button.map-button { } button.map-button:hover { - border-color: var(--rs-gray-0); + border-color: var(--surface-color-border-hover); + background-color: var(--surface-background-white); } button.map-button.map-button--large { @@ -104,12 +105,14 @@ button.map-button.map-button--medium { } button.map-button--active { - background-color: var(--rs-gray-0); + /* background-color: var(--rs-gray-0); */ + background-color: var(--surface-color-secondary-hover); border: 3px solid var(--surface-color-primary-active-border); } button.map-button--active:hover { - border-color: var(--rs-gray-0); + border-color: var(--surface-color-primary-active-border); + background-color: var(--surface-color-secondary-hover); } .data-layers-icon, diff --git a/frontend/src/app/features/map/MapView.tsx b/frontend/src/app/features/map/MapView.tsx index e1808889..d76429f1 100644 --- a/frontend/src/app/features/map/MapView.tsx +++ b/frontend/src/app/features/map/MapView.tsx @@ -14,6 +14,7 @@ import { MapSearch } from './MapSearch'; import { useMapSearchQuery } from '../../../graphql/generated'; import { SiteMarkers } from './siteMarkers/SiteMarkers'; import { SiteDetailsDrawer } from './siteDrawer/SiteDetailsDrawer'; +import { MapControls } from './MapControls'; // Set the position of the marker for center of BC const CENTER_OF_BC: LatLngTuple = [53.7267, -127.6476]; @@ -49,7 +50,8 @@ function MapView() { url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" className={clsx(osmGrayscale && 'osm--grayscale')} /> - {/* */} + + diff --git a/frontend/src/app/features/map/MyLocationMarker.tsx b/frontend/src/app/features/map/MyLocationMarker.tsx index 141bc18a..318ecc2e 100644 --- a/frontend/src/app/features/map/MyLocationMarker.tsx +++ b/frontend/src/app/features/map/MyLocationMarker.tsx @@ -2,11 +2,11 @@ import { useEffect, useRef } from 'react'; import L from 'leaflet'; import { Circle, Tooltip, useMap } from 'react-leaflet'; -//import { useMyLocationVisible } from '@/features/map/map-slice' import { useMyLocation } from '../../../hooks/useMyLocation'; import { IconMarker } from './IconMarker'; import './MyLocationMarker.css'; +import { useMyLocationVisible } from './map-slice'; export function myLocationIcon() { const size = 24; @@ -41,7 +41,6 @@ function MyLocationMarkerContent() { } }, [map, position]); - console.log('nupur - MyLocationMarkerContent is at position: ', position); return position ? ( <> {Math.round(accuracy) >= 1 && ( @@ -52,8 +51,6 @@ function MyLocationMarkerContent() { /> )} - console.log("nupur - MyLocationMarkerContent is at position: ", - position) My location @@ -61,8 +58,6 @@ function MyLocationMarkerContent() { } export function MyLocationMarker() { - //TODO - Implement useMyLocationVisible - // const isVisible = useMyLocationVisible() - // return isVisible ? : null - return ; + const isVisible = useMyLocationVisible(); + return isVisible ? : null; } diff --git a/frontend/src/app/features/map/map-slice.ts b/frontend/src/app/features/map/map-slice.ts new file mode 100644 index 00000000..7ec81b6c --- /dev/null +++ b/frontend/src/app/features/map/map-slice.ts @@ -0,0 +1,28 @@ +import { useSelector } from 'react-redux'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface MapSliceState { + isMyLocationVisible: boolean; +} + +export const initialState: MapSliceState = { + isMyLocationVisible: false, +}; + +export const mapSlice = createSlice({ + name: 'map', + initialState, + reducers: { + setMyLocationVisible: (state, action: PayloadAction) => { + state.isMyLocationVisible = action.payload; + }, + }, +}); + +export const { setMyLocationVisible } = mapSlice.actions; + +// Selectors +const selectMyLocationVisible = (state: any) => state.map.isMyLocationVisible; +export const useMyLocationVisible = () => useSelector(selectMyLocationVisible); + +export default mapSlice.reducer;