Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: SRS-527 Map Search – Find Me #186

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/app/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -39,6 +40,7 @@ export const store = configureStore({
parcelDescriptions: parcelDescriptionsReducer,
srUpdates: srUpdatesReducer,
srReview: srReviewReducer,
map: mapReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
Expand Down
80 changes: 80 additions & 0 deletions frontend/src/app/features/map/Control.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
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<HTMLElement>(
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(
() => (
<div
className={clsx('leaflet-control leaflet-bar', className)}
style={style}
>
{children}
</div>
),
[children, style],
);

return createPortal(controlContainer, container);
}
33 changes: 14 additions & 19 deletions frontend/src/app/features/map/FindMeButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button
Expand All @@ -32,10 +27,10 @@ export function FindMeButton() {
className={clsx(
'map-button',
'map-button--large',
// isMarkerVisible && 'map-button--active',
isMarkerVisible && 'map-button--active',
)}
startIcon={<FindMe title="Find me icon" className="find-me-icon" />}
// onClick={onClick}
onClick={onClick}
>
Find Me
</Button>
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/app/features/map/FindMeControl.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FindMeControl and FindMeButton are almost identical. To avoid code duplication, they should be combined into a single component that accepts settings for its appearance/position as props.

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 (
<IconButton
className={clsx(
'map-control-button',
isMarkerVisible && 'map-control-button--active',
)}
onClick={onClick}
title="Show my location on the map"
>
<FindMe />
</IconButton>
);
}
42 changes: 42 additions & 0 deletions frontend/src/app/features/map/MapControl.css
Original file line number Diff line number Diff line change
@@ -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);
}
49 changes: 49 additions & 0 deletions frontend/src/app/features/map/MapControls.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Control position="bottomright" className="map-controls" style={style}>
{isMedium && <FindMeControl />}
</Control>
);
}
9 changes: 6 additions & 3 deletions frontend/src/app/features/map/MapSearch.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/app/features/map/MapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -49,7 +50,8 @@ function MapView() {
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
className={clsx(osmGrayscale && 'osm--grayscale')}
/>
{/* <MyLocationMarker/> */}
<MapControls />
<MyLocationMarker />
<SiteMarkers sites={data?.mapSearch.data || []} />
</MapContainer>
<MapSearch />
Expand Down
11 changes: 3 additions & 8 deletions frontend/src/app/features/map/MyLocationMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,7 +41,6 @@ function MyLocationMarkerContent() {
}
}, [map, position]);

console.log('nupur - MyLocationMarkerContent is at position: ', position);
return position ? (
<>
{Math.round(accuracy) >= 1 && (
Expand All @@ -52,17 +51,13 @@ function MyLocationMarkerContent() {
/>
)}
<IconMarker position={position} icon={locationIcon} zIndexOffset={1000}>
console.log("nupur - MyLocationMarkerContent is at position: ",
position)
<Tooltip direction="top">My location</Tooltip>
</IconMarker>
</>
) : null;
}

export function MyLocationMarker() {
//TODO - Implement useMyLocationVisible
// const isVisible = useMyLocationVisible()
// return isVisible ? <MyLocationMarkerContent /> : null
return <MyLocationMarkerContent />;
const isVisible = useMyLocationVisible();
return isVisible ? <MyLocationMarkerContent /> : null;
}
28 changes: 28 additions & 0 deletions frontend/src/app/features/map/map-slice.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that hooking into redux for a single boolean field is a massive overkill and a regular state field on the level of <MapView /> component should do the trick just fine.

You mentioned that you were getting map context errors before the redux implementation, but looking at the code just now, I'm 99% certain those were unrelated. Happy to hop on a call to debug this together

Original file line number Diff line number Diff line change
@@ -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<boolean>) => {
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;
Loading