diff --git a/packages/components/src/MapWithPin/MapPin.client.tsx b/packages/components/src/MapWithPin/MapPin.client.tsx index 6e75ab7306..5f9ca76662 100644 --- a/packages/components/src/MapWithPin/MapPin.client.tsx +++ b/packages/components/src/MapWithPin/MapPin.client.tsx @@ -1,9 +1,11 @@ -import * as React from 'react' +import { useRef } from 'react' import { Marker } from 'react-leaflet' import L from 'leaflet' import customMarkerIcon from '../../assets/icons/map-marker.png' +import type { DivIcon } from 'leaflet' + const customMarker = L.icon({ iconUrl: customMarkerIcon, iconSize: [20, 28], @@ -15,31 +17,35 @@ 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 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) - } - }} + draggable + onDrag={handleDrag} position={[props.position.lat, props.position.lng]} ref={markerRef} - icon={customMarker} + icon={props.markerIcon || customMarker} + onclick={props.onClick} /> ) } 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..0f28de3dab 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 && ( { } } -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.test.tsx b/src/pages/UserSettings/SettingsPageMapPin.test.tsx index d24c3b4309..a83779f321 100644 --- a/src/pages/UserSettings/SettingsPageMapPin.test.tsx +++ b/src/pages/UserSettings/SettingsPageMapPin.test.tsx @@ -24,6 +24,21 @@ vi.mock('src/common/hooks/useCommonStores', () => ({ }, 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..8654660a2f 100644 --- a/src/pages/UserSettings/SettingsPageMapPin.tsx +++ b/src/pages/UserSettings/SettingsPageMapPin.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, 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 = 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, true)) setIsLoading(false) } @@ -289,11 +302,25 @@ export const SettingsPageMapPin = () => { 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/pages/UserSettings/index.tsx b/src/pages/UserSettings/index.tsx index 74d9b55d87..019bda5fda 100644 --- a/src/pages/UserSettings/index.tsx +++ b/src/pages/UserSettings/index.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react' import { Loader } from 'oa-components' import { useCommonStores } from 'src/common/hooks/useCommonStores' -import { SettingsPage } from './SettingsPage' +import { SettingsPage } from './SettingsPage.client' import type { IUser } from 'oa-shared' diff --git a/src/routes/_.settings.$.tsx b/src/routes/_.settings.$.tsx index 1c7f29b518..f1bce01db5 100644 --- a/src/routes/_.settings.$.tsx +++ b/src/routes/_.settings.$.tsx @@ -1,7 +1,7 @@ import { observer } from 'mobx-react' import { useCommonStores } from 'src/common/hooks/useCommonStores' import Main from 'src/pages/common/Layout/Main' -import { SettingsPage } from 'src/pages/UserSettings/SettingsPage' +import { SettingsPage } from 'src/pages/UserSettings/SettingsPage.client' import { Flex, Text } from 'theme-ui' import type { IUser } from 'oa-shared' diff --git a/src/routes/_.u.$id.edit.tsx b/src/routes/_.u.$id.edit.tsx index cae6db2253..4c951df09a 100644 --- a/src/routes/_.u.$id.edit.tsx +++ b/src/routes/_.u.$id.edit.tsx @@ -1,7 +1,7 @@ import { UserRole } from 'oa-shared' import { AuthRoute } from 'src/pages/common/AuthRoute' import Main from 'src/pages/common/Layout/Main' -import { SettingsPage } from 'src/pages/UserSettings/SettingsPage' +import { SettingsPage } from 'src/pages/UserSettings/SettingsPage.client' import { SeoTagsUpdateComponent } from 'src/utils/seo' export async function clientLoader() { 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