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 f9bcd27225..aa838a9758 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/MapPin.tsx b/packages/components/src/MapWithPin/MapPin.tsx index 6e75ab7306..cfe6938be1 100644 --- a/packages/components/src/MapWithPin/MapPin.tsx +++ b/packages/components/src/MapWithPin/MapPin.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/MapWithPin.stories.tsx b/packages/components/src/MapWithPin/MapWithPin.stories.tsx index 7b9273c667..119164dc47 100644 --- a/packages/components/src/MapWithPin/MapWithPin.stories.tsx +++ b/packages/components/src/MapWithPin/MapWithPin.stories.tsx @@ -1,6 +1,9 @@ +import React from 'react' + import { MapWithPin } from './MapWithPin' 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 = React.useRef(null) + return ( { position.lat = _position.lat position.lng = _position.lng diff --git a/packages/components/src/MapWithPin/MapWithPin.tsx b/packages/components/src/MapWithPin/MapWithPin.tsx index 3d8b9cbe67..6ba936a839 100644 --- a/packages/components/src/MapWithPin/MapWithPin.tsx +++ b/packages/components/src/MapWithPin/MapWithPin.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' import { OsmGeocoding } from '../OsmGeocoding/OsmGeocoding' import { MapPin } from './MapPin' -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/src/models/common.models.tsx b/src/models/common.models.tsx index 1bac0512b8..c4976751fc 100644 --- a/src/models/common.models.tsx +++ b/src/models/common.models.tsx @@ -23,7 +23,8 @@ export interface ILocation { postcode: string value: string } -interface ILatLng { + +export interface ILatLng { lat: number lng: number } diff --git a/src/models/index.ts b/src/models/index.ts index 57f5dea5b6..f590d17e69 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,5 +1,3 @@ -import type { IComment } from './discussion.models' - export * from './common.models' export * from './discussion.models' export * from './howto.models' @@ -17,3 +15,4 @@ export * from './moderation.model' export interface UserComment extends IComment { isEditable: boolean } +export * from './userPreciousPlastic.models' diff --git a/src/pages/Maps/Content/MapView/Popup.tsx b/src/pages/Maps/Content/MapView/Popup.tsx index 1fe8717387..5c1c6b8e6e 100644 --- a/src/pages/Maps/Content/MapView/Popup.tsx +++ b/src/pages/Maps/Content/MapView/Popup.tsx @@ -5,6 +5,7 @@ import { MapMemberCard, PinProfile } from 'oa-components' import { IModerationStatus } from 'oa-shared' import { MAP_GROUPINGS } from 'src/stores/Maps/maps.groupings' +import type { ILatLng } from 'oa-shared' import type { Map } from 'react-leaflet' import type { IMapPin, IMapPinWithDetail } from 'src/models/maps.models' @@ -15,12 +16,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 +49,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 4c1b3945e4..f092156d3f 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,23 @@ 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' +import { createMarkerIcon } from '../Maps/Content/MapView/Sprites' import { SettingsFormNotifications } from './content/SettingsFormNotifications' import { MAX_PIN_LENGTH } from './constants' -import type { ILocation, IMapPin, IUserDB } from 'src/models' +import type { DivIcon } from 'leaflet' +import type { Map } from 'react-leaflet' +import type { + ILatLng, + ILocation, + IMapPinWithDetail, + IUserPPDB, +} from 'src/models' import type { IFormNotification } from './content/SettingsFormNotifications' interface IPinProps { - mapPin: IMapPin | undefined + mapPin: IMapPinWithDetail | undefined } interface ILocationProps { @@ -145,7 +154,9 @@ const DeleteMapPin = (props: IPropsDeletePin) => { } export const SettingsPageMapPin = () => { - 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 @@ -153,17 +164,28 @@ export const SettingsPageMapPin = () => { const { mapsStore, themeStore, 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 + + const currentTheme = themeStore.currentTheme + + const newMapRef = React.useRef(null) 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, currentTheme)) setIsLoading(false) } @@ -223,7 +245,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 9ba01743e2..e085d5fe74 100644 --- a/src/stores/Maps/maps.store.ts +++ b/src/stores/Maps/maps.store.ts @@ -139,7 +139,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