From ff722fb67c506f0dbf1b84bda276a697c3f6b797 Mon Sep 17 00:00:00 2001 From: Lahuen Garcia Date: Thu, 26 Sep 2024 13:06:13 -0300 Subject: [PATCH 1/2] feat(component): update map settings display Pin is now like the Map page and, when clicked, displays the popup fix #3856 --- .../src/MapWithPin/MapPin.client.tsx | 49 +++--- packages/components/src/MapWithPin/MapPin.css | 7 + .../src/MapWithPin/MapPin.stories.tsx | 3 +- .../src/MapWithPin/MapWithPin.client.tsx | 142 ++++++++++-------- .../src/MapWithPin/MapWithPin.stories.tsx | 7 +- .../Maps/Content/MapView/Popup.client.tsx | 13 +- .../UserSettings/SettingsPageMapPin.test.tsx | 15 ++ src/pages/UserSettings/SettingsPageMapPin.tsx | 49 ++++-- src/stores/Maps/maps.store.ts | 2 +- 9 files changed, 187 insertions(+), 100 deletions(-) create mode 100644 packages/components/src/MapWithPin/MapPin.css diff --git a/packages/components/src/MapWithPin/MapPin.client.tsx b/packages/components/src/MapWithPin/MapPin.client.tsx index 6e75ab7306..cfe6938be1 100644 --- a/packages/components/src/MapWithPin/MapPin.client.tsx +++ b/packages/components/src/MapWithPin/MapPin.client.tsx @@ -1,9 +1,13 @@ -import * as React from 'react' +import { useRef, useState } from 'react' import { Marker } from 'react-leaflet' import L from 'leaflet' import customMarkerIcon from '../../assets/icons/map-marker.png' +import type { DivIcon } from 'leaflet' + +import './MapPin.css' + const customMarker = L.icon({ iconUrl: customMarkerIcon, iconSize: [20, 28], @@ -15,31 +19,40 @@ export interface IProps { lat: number lng: number } - draggable: boolean - ondragend(lng: number): void + onDrag(lng: number): void + markerIcon?: DivIcon + onClick?: () => void } export const MapPin = (props: IProps) => { - const markerRef = React.useRef(null) + const markerRef = useRef(null) + const [isDragging, setIsDragging] = useState(false) + + const handleDrag = () => { + const marker: any = markerRef.current + + if (!marker) { + return + } + + const markerLatLng = marker.leafletElement.getLatLng() + if (props.onDrag) { + props.onDrag(markerLatLng) + } + } return ( { - const marker: any = markerRef.current - - if (!marker) { - return null - } - - const markerLatLng = marker.leafletElement.getLatLng() - if (props.ondragend) { - props.ondragend(markerLatLng) - } - }} + className={`map-pin ${isDragging ? 'dragging' : ''}`} + draggable + onDrag={handleDrag} position={[props.position.lat, props.position.lng]} ref={markerRef} - icon={customMarker} + icon={props.markerIcon || customMarker} + onMouseDown={() => setIsDragging(true)} + onMouseUp={() => setIsDragging(false)} + onMouseLeave={() => setIsDragging(false)} + onclick={props.onClick} /> ) } diff --git a/packages/components/src/MapWithPin/MapPin.css b/packages/components/src/MapWithPin/MapPin.css new file mode 100644 index 0000000000..077f5c4841 --- /dev/null +++ b/packages/components/src/MapWithPin/MapPin.css @@ -0,0 +1,7 @@ +.leaflet-marker-icon:hover { + cursor: grab !important; +} + +.leaflet-drag-target { + cursor: grabbing; +} diff --git a/packages/components/src/MapWithPin/MapPin.stories.tsx b/packages/components/src/MapWithPin/MapPin.stories.tsx index b2857d3af6..83c39c57ee 100644 --- a/packages/components/src/MapWithPin/MapPin.stories.tsx +++ b/packages/components/src/MapWithPin/MapPin.stories.tsx @@ -12,8 +12,7 @@ export const Default: StoryFn = () => { return ( { + onDrag={(lng: number) => { position.lng = lng }} /> diff --git a/packages/components/src/MapWithPin/MapWithPin.client.tsx b/packages/components/src/MapWithPin/MapWithPin.client.tsx index b7973fad0c..aedfce8bea 100644 --- a/packages/components/src/MapWithPin/MapWithPin.client.tsx +++ b/packages/components/src/MapWithPin/MapWithPin.client.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { ZoomControl } from 'react-leaflet' import { Alert, Box, Flex, Text } from 'theme-ui' @@ -7,33 +7,38 @@ import { Map } from '../Map/Map.client' import { OsmGeocoding } from '../OsmGeocoding/OsmGeocoding' import { MapPin } from './MapPin.client' -import type { LeafletMouseEvent } from 'leaflet' +import type { DivIcon, LeafletMouseEvent } from 'leaflet' +import type { Map as MapType } from 'react-leaflet' import type { Result } from '../OsmGeocoding/types' import 'leaflet/dist/leaflet.css' const useUserLocation = 'Use my current location' const mapInstructions = - "You can click on the map, or drag the marker to adjust it's position." + "To move your pin, grab it to move it or double click where you want it to go. Tap on your pin to see how it'll look on the map." export interface Props { + mapRef: React.RefObject position: { lat: number lng: number } - draggable: boolean + markerIcon?: DivIcon updatePosition?: any center?: any zoom?: number hasUserLocation?: boolean + onClickMapPin?: () => void + popup?: React.ReactNode } export const MapWithPin = (props: Props) => { - const [zoom, setZoom] = React.useState(props.zoom || 1) - const [center, setCenter] = React.useState( + const [dragging, setDragging] = useState(false) + const [zoom, setZoom] = useState(props.zoom || 1) + const [center, setCenter] = useState( props.center || [props.position.lat, props.position.lng], ) - const { draggable, position } = props + const { mapRef, position, markerIcon, onClickMapPin, popup } = props const hasUserLocation = props.hasUserLocation || false const onPositionChanged = @@ -58,22 +63,20 @@ export const MapWithPin = (props: Props) => { ) } - const onClick = (evt: LeafletMouseEvent) => { + const onDblClick = (evt: LeafletMouseEvent) => { onPositionChanged({ ...evt.latlng }) } return ( - {draggable && ( - - {mapInstructions} - - )} + + {mapInstructions} +
{ - {draggable && ( - - { - if (data.lat && data.lon) { - onPositionChanged({ - lat: data.lat, - lng: data.lon, - }) - setCenter([data.lat, data.lon]) - setZoom(15) - } + + { + if (data.lat && data.lon) { + onPositionChanged({ + lat: data.lat, + lng: data.lon, + }) + setCenter([data.lat, data.lon]) + setZoom(15) + } + }} + acceptLanguage="en" + /> + {hasUserLocation && ( + - )} - - )} + > + {useUserLocation} + + )} + setDragging(true)} + ondragend={() => setDragging(false)} > - - { - if (evt.lat && evt.lng) - onPositionChanged({ - lat: evt.lat, - lng: evt.lng, - }) - }} - /> + + {!dragging && ( + <> + {popup} + { + if (evt.lat && evt.lng) + onPositionChanged({ + lat: evt.lat, + lng: evt.lng, + }) + }} + /> + + )}
diff --git a/packages/components/src/MapWithPin/MapWithPin.stories.tsx b/packages/components/src/MapWithPin/MapWithPin.stories.tsx index f2376c5512..e6526eb57f 100644 --- a/packages/components/src/MapWithPin/MapWithPin.stories.tsx +++ b/packages/components/src/MapWithPin/MapWithPin.stories.tsx @@ -1,6 +1,9 @@ +import { useRef } from 'react' + import { MapWithPin } from './MapWithPin.client' import type { Meta, StoryFn } from '@storybook/react' +import type { Map } from 'react-leaflet' export default { title: 'Map/MapWithPin', @@ -9,10 +12,12 @@ export default { export const Default: StoryFn = () => { const position = { lat: 0, lng: 0 } + const newMapRef = useRef(null) + return ( { position.lat = _position.lat position.lng = _position.lng diff --git a/src/pages/Maps/Content/MapView/Popup.client.tsx b/src/pages/Maps/Content/MapView/Popup.client.tsx index 2f9aa25aaa..e51fb51c00 100644 --- a/src/pages/Maps/Content/MapView/Popup.client.tsx +++ b/src/pages/Maps/Content/MapView/Popup.client.tsx @@ -5,7 +5,7 @@ import { MapMemberCard, PinProfile } from 'oa-components' import { IModerationStatus } from 'oa-shared' import { MAP_GROUPINGS } from 'src/stores/Maps/maps.groupings' -import type { IMapPin, IMapPinWithDetail } from 'oa-shared' +import type { ILatLng, IMapPin, IMapPinWithDetail } from 'oa-shared' import type { Map } from 'react-leaflet' import './popup.css' @@ -15,12 +15,13 @@ interface IProps { mapRef: React.RefObject newMap?: boolean onClose?: () => void + customPosition?: ILatLng } export const Popup = (props: IProps) => { const leafletRef = useRef(null) const activePin = props.activePin as IMapPinWithDetail - const { mapRef, newMap, onClose } = props + const { mapRef, newMap, onClose, customPosition } = props useEffect(() => { openPopup() @@ -47,8 +48,14 @@ export const Popup = (props: IProps) => { activePin.location && ( ({ }, mapsStore: { getPin: vi.fn().mockResolvedValue(mockPin), + getPinDetail: vi.fn().mockResolvedValue(mockPin), + }, + themeStore: { + currentTheme: { + id: 'string', + siteName: 'string', + logo: 'string', + badge: 'string', + avatar: 'string', + howtoHeading: 'string', + academyResource: 'string', + styles: { + communityProgramURL: '', + }, + }, }, }, }), diff --git a/src/pages/UserSettings/SettingsPageMapPin.tsx b/src/pages/UserSettings/SettingsPageMapPin.tsx index b52b78e763..c6d09dcd6e 100644 --- a/src/pages/UserSettings/SettingsPageMapPin.tsx +++ b/src/pages/UserSettings/SettingsPageMapPin.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { Field, Form } from 'react-final-form' import { toJS } from 'mobx' import { @@ -22,14 +22,18 @@ import { randomIntFromInterval } from 'src/utils/helpers' import { required } from 'src/utils/validators' import { Alert, Box, Flex, Heading, Text } from 'theme-ui' +import { Popup } from '../Maps/Content/MapView/Popup.client' +import { createMarkerIcon } from '../Maps/Content/MapView/Sprites' import { SettingsFormNotifications } from './content/SettingsFormNotifications' import { MAX_PIN_LENGTH } from './constants' -import type { ILocation, IMapPin, IUserDB } from 'oa-shared' +import type { DivIcon } from 'leaflet' +import type { ILatLng, ILocation, IMapPinWithDetail, IUserDB } from 'oa-shared' +import type { Map } from 'react-leaflet' import type { IFormNotification } from './content/SettingsFormNotifications' interface IPinProps { - mapPin: IMapPin | undefined + mapPin: IMapPinWithDetail | undefined } interface ILocationProps { @@ -148,25 +152,34 @@ export const SettingsPageMapPin = () => { const communityProgramUrl = import.meta.env.VITE_COMMUNITY_PROGRAM_URL || process.env.VITE_COMMUNITY_PROGRAM_URL - const [mapPin, setMapPin] = useState() + const [mapPin, setMapPin] = useState() + const [markerIcon, setMarkerIcon] = useState() + const [showPopup, setShowPopup] = useState(false) const [isLoading, setIsLoading] = useState(true) const [notification, setNotification] = useState< IFormNotification | undefined >(undefined) + const newMapRef = React.useRef(null) + const { mapsStore, userStore } = useCommonStores().stores const user = userStore.activeUser + const isMember = user?.profileType === ProfileTypeList.MEMBER const { addPinTitle, yourPinTitle } = headings.map - const formId = 'MapSection' - const isMember = user?.profileType === ProfileTypeList.MEMBER useEffect(() => { const init = async () => { if (!user) return - const pin = (await mapsStore.getPin(user.userName)) || null - setMapPin(pin) + const pin = await mapsStore.getPin(user.userName) + if (!pin) return + + const pinDetail = await mapsStore.getPinDetail(pin) + if (!pinDetail) return + + setMapPin(pinDetail) + setMarkerIcon(createMarkerIcon(pin)) setIsLoading(false) } @@ -226,7 +239,7 @@ export const SettingsPageMapPin = () => { > - {mapPin ? addPinTitle : yourPinTitle} + {mapPin ? yourPinTitle : addPinTitle} {isMember && ( { return ( { + updatePosition={(newPosition: ILatLng) => { onChange({ latlng: newPosition }) }} + markerIcon={markerIcon} + zoom={mapPin ? 15 : 1} + onClickMapPin={() => setShowPopup(!showPopup)} + popup={ + mapPin && showPopup ? ( + setShowPopup(!showPopup)} + customPosition={location.latlng || undefined} + /> + ) : undefined + } /> ) }} diff --git a/src/stores/Maps/maps.store.ts b/src/stores/Maps/maps.store.ts index 5c2defcca3..bb1dbeaccb 100644 --- a/src/stores/Maps/maps.store.ts +++ b/src/stores/Maps/maps.store.ts @@ -141,7 +141,7 @@ export class MapsStore extends ModuleStore { } } // call additional action when pin detail received to inform mobx correctly of update - private async getPinDetail(pin: IMapPin) { + public async getPinDetail(pin: IMapPin) { const detail: IMapPinDetail = await this.getUserProfilePin(pin._id) const pinWithDetail: IMapPinWithDetail = { ...pin, detail } return pinWithDetail From 4853c550649da08838e79a413a8502c260c3ea7e Mon Sep 17 00:00:00 2001 From: Lahuen Garcia Date: Wed, 25 Sep 2024 04:13:32 -0300 Subject: [PATCH 2/2] feat(component): update map settings display Pin is now like the Map page and, when clicked, displays the popup fix #3856 --- packages/components/src/MapWithPin/MapPin.client.tsx | 9 +-------- packages/components/src/MapWithPin/MapPin.css | 7 ------- packages/components/src/MapWithPin/MapWithPin.client.tsx | 2 +- src/pages/Maps/Content/MapView/Sprites.tsx | 4 ++-- src/pages/User/user.routes.tsx | 2 +- .../{SettingsPage.tsx => SettingsPage.client.tsx} | 0 src/pages/UserSettings/SettingsPageMapPin.tsx | 8 ++++---- src/pages/UserSettings/index.tsx | 2 +- src/routes/_.settings.$.tsx | 2 +- src/routes/_.u.$id.edit.tsx | 2 +- 10 files changed, 12 insertions(+), 26 deletions(-) delete mode 100644 packages/components/src/MapWithPin/MapPin.css rename src/pages/UserSettings/{SettingsPage.tsx => SettingsPage.client.tsx} (100%) diff --git a/packages/components/src/MapWithPin/MapPin.client.tsx b/packages/components/src/MapWithPin/MapPin.client.tsx index cfe6938be1..5f9ca76662 100644 --- a/packages/components/src/MapWithPin/MapPin.client.tsx +++ b/packages/components/src/MapWithPin/MapPin.client.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react' +import { useRef } from 'react' import { Marker } from 'react-leaflet' import L from 'leaflet' @@ -6,8 +6,6 @@ import customMarkerIcon from '../../assets/icons/map-marker.png' import type { DivIcon } from 'leaflet' -import './MapPin.css' - const customMarker = L.icon({ iconUrl: customMarkerIcon, iconSize: [20, 28], @@ -26,7 +24,6 @@ export interface IProps { export const MapPin = (props: IProps) => { const markerRef = useRef(null) - const [isDragging, setIsDragging] = useState(false) const handleDrag = () => { const marker: any = markerRef.current @@ -43,15 +40,11 @@ export const MapPin = (props: IProps) => { return ( setIsDragging(true)} - onMouseUp={() => setIsDragging(false)} - onMouseLeave={() => setIsDragging(false)} onclick={props.onClick} /> ) diff --git a/packages/components/src/MapWithPin/MapPin.css b/packages/components/src/MapWithPin/MapPin.css deleted file mode 100644 index 077f5c4841..0000000000 --- a/packages/components/src/MapWithPin/MapPin.css +++ /dev/null @@ -1,7 +0,0 @@ -.leaflet-marker-icon:hover { - cursor: grab !important; -} - -.leaflet-drag-target { - cursor: grabbing; -} diff --git a/packages/components/src/MapWithPin/MapWithPin.client.tsx b/packages/components/src/MapWithPin/MapWithPin.client.tsx index aedfce8bea..0f28de3dab 100644 --- a/packages/components/src/MapWithPin/MapWithPin.client.tsx +++ b/packages/components/src/MapWithPin/MapWithPin.client.tsx @@ -126,7 +126,7 @@ export const MapWithPin = (props: Props) => { { } } -export const createMarkerIcon = (pin: IMapPin) => { +export const createMarkerIcon = (pin: IMapPin, draggable?: boolean) => { const icon = pin.moderation === IModerationStatus.ACCEPTED ? Workspace.findWorkspaceBadge(pin.type, true, pin.verified) @@ -45,7 +45,7 @@ export const createMarkerIcon = (pin: IMapPin) => { } return L.divIcon({ className: `icon-marker icon-${pin.type}`, - html: ``, + html: ``, iconSize: L.point(38, 38, true), }) } diff --git a/src/pages/User/user.routes.tsx b/src/pages/User/user.routes.tsx index 9e1fe01dc9..3204a0d11b 100644 --- a/src/pages/User/user.routes.tsx +++ b/src/pages/User/user.routes.tsx @@ -3,7 +3,7 @@ import { UserRole } from 'oa-shared' import { AuthRoute } from '../common/AuthRoute' import { NotFoundPage } from '../NotFound/NotFound' -import { SettingsPage } from '../UserSettings/SettingsPage' +import { SettingsPage } from '../UserSettings/SettingsPage.client' import { UserProfile } from './content/UserProfile' export const UserRoutes = ( diff --git a/src/pages/UserSettings/SettingsPage.tsx b/src/pages/UserSettings/SettingsPage.client.tsx similarity index 100% rename from src/pages/UserSettings/SettingsPage.tsx rename to src/pages/UserSettings/SettingsPage.client.tsx diff --git a/src/pages/UserSettings/SettingsPageMapPin.tsx b/src/pages/UserSettings/SettingsPageMapPin.tsx index c6d09dcd6e..8654660a2f 100644 --- a/src/pages/UserSettings/SettingsPageMapPin.tsx +++ b/src/pages/UserSettings/SettingsPageMapPin.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { Field, Form } from 'react-final-form' import { toJS } from 'mobx' import { @@ -160,7 +160,7 @@ export const SettingsPageMapPin = () => { IFormNotification | undefined >(undefined) - const newMapRef = React.useRef(null) + const newMapRef = useRef(null) const { mapsStore, userStore } = useCommonStores().stores const user = userStore.activeUser @@ -179,7 +179,7 @@ export const SettingsPageMapPin = () => { if (!pinDetail) return setMapPin(pinDetail) - setMarkerIcon(createMarkerIcon(pin)) + setMarkerIcon(createMarkerIcon(pin, true)) setIsLoading(false) } @@ -239,7 +239,7 @@ export const SettingsPageMapPin = () => { > - {mapPin ? yourPinTitle : addPinTitle} + {mapPin ? addPinTitle : yourPinTitle} {isMember && (