From a1b33f35df21d6b24ece7e25280b5a93bc41b069 Mon Sep 17 00:00:00 2001 From: LahuenGR <101137877+LahuenGR@users.noreply.github.com> Date: Wed, 20 Nov 2024 07:04:06 -0300 Subject: [PATCH] feat: update map setting display (#3862) --- .../src/MapWithPin/MapPin.client.tsx | 42 +++--- .../src/MapWithPin/MapPin.stories.tsx | 3 +- .../src/MapWithPin/MapWithPin.client.tsx | 139 ++++++++++-------- .../src/MapWithPin/MapWithPin.stories.tsx | 7 +- .../cypress/src/integration/settings.spec.ts | 8 +- packages/cypress/src/support/commandsUi.ts | 2 - .../Maps/Content/MapView/Popup.client.tsx | 13 +- src/pages/Maps/Content/MapView/Sprites.tsx | 4 +- src/pages/User/user.routes.tsx | 2 +- ...ttingsPage.tsx => SettingsPage.client.tsx} | 0 .../UserSettings/SettingsPageMapPin.test.tsx | 15 ++ src/pages/UserSettings/SettingsPageMapPin.tsx | 112 +++++++------- src/pages/UserSettings/index.tsx | 2 +- src/pages/UserSettings/labels.ts | 6 - src/routes/_.settings.$.tsx | 2 +- src/routes/_.u.$id.edit.tsx | 2 +- src/stores/Maps/maps.store.ts | 2 +- 17 files changed, 196 insertions(+), 165 deletions(-) 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 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..a036bc73e1 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,37 @@ 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.' 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 [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 +62,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} + + )} + - - { - if (evt.lat && evt.lng) - onPositionChanged({ - lat: evt.lat, - lng: evt.lng, - }) - }} - /> + + <> + {popup} + {position && ( + { + 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/packages/cypress/src/integration/settings.spec.ts b/packages/cypress/src/integration/settings.spec.ts index 618f334908..2544407cdc 100644 --- a/packages/cypress/src/integration/settings.spec.ts +++ b/packages/cypress/src/integration/settings.spec.ts @@ -13,11 +13,10 @@ const locationStub = { value: 'Singapore', } -const mapDetails = (description) => ({ - description, +const mapDetails = { searchKeyword: 'singapo', locationName: locationStub.value, -}) +} describe('[Settings]', () => { beforeEach(() => { @@ -51,7 +50,6 @@ describe('[Settings]', () => { const userImage = 'avatar' const displayName = 'settings_member_new' const description = "I'm a very active member" - const mapPinDescription = 'Fun, vibrant and full of amazing people' const profileType = 'member' const tag = ['Sewing', 'Accounting'] const user = generateNewUserDetails() @@ -148,7 +146,7 @@ describe('[Settings]', () => { cy.get('[data-cy="tab-Map"]').click() cy.get('[data-cy=descriptionMember').should('be.visible') cy.contains('No map pin currently saved') - cy.fillSettingMapPin(mapDetails(mapPinDescription)) + cy.fillSettingMapPin(mapDetails) cy.get('[data-cy=save-map-pin]').click() cy.contains('Map pin saved successfully') cy.contains('Your current map pin is here:') diff --git a/packages/cypress/src/support/commandsUi.ts b/packages/cypress/src/support/commandsUi.ts index 48bc36cadb..5c8cd5e472 100644 --- a/packages/cypress/src/support/commandsUi.ts +++ b/packages/cypress/src/support/commandsUi.ts @@ -18,7 +18,6 @@ interface IInfo { type ILink = Omit interface IMapPin { - description: string searchKeyword: string locationName: string } @@ -144,7 +143,6 @@ Cypress.Commands.add('fillSettingMapPin', (mapPin: IMapPin) => { cy.wait('@fetchAddress').then(() => { cy.get('[data-cy="osm-geocoding-results"]').find('li:eq(0)').click() }) - cy.get('[data-cy=pin-description]').clear().type(mapPin.description) }) Cypress.Commands.add('setSettingPublicContact', () => { 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 f45f78d6ba..6fd052c7e1 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' import type { IUserDB } from 'oa-shared' 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 a62d00e7f1..2591a68a7e 100644 --- a/src/pages/UserSettings/SettingsPageMapPin.tsx +++ b/src/pages/UserSettings/SettingsPageMapPin.tsx @@ -1,42 +1,37 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { Field, Form } from 'react-final-form' +import { useNavigate } from 'react-router' import { toJS } from 'mobx' import { Button, ConfirmModal, ExternalLink, - FieldTextarea, FlagIconEvents, Loader, MapWithPin, } from 'oa-components' import { IModerationStatus, ProfileTypeList } from 'oa-shared' import { useCommonStores } from 'src/common/hooks/useCommonStores' -import { - buttons, - fields, - headings, - mapForm, -} from 'src/pages/UserSettings/labels' +import { buttons, headings, mapForm } from 'src/pages/UserSettings/labels' import { randomIntFromInterval } from 'src/utils/helpers' -import { required } from 'src/utils/validators' import { Alert, Box, Flex, Heading, Text } from 'theme-ui' +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, IMapPin, IUserDB } from 'oa-shared' +import type { Map } from 'react-leaflet' import type { IFormNotification } from './content/SettingsFormNotifications' interface IPinProps { mapPin: IMapPin | undefined } -interface ILocationProps { - location: IUserDB['location'] -} +const LocationDataTextDisplay = ({ user }: { user: IUserDB }) => { + const { _id, location } = user + const navigate = useNavigate() -const LocationDataTextDisplay = ({ location }: ILocationProps) => { if (!location?.latlng) return ( { ) return ( - - {mapForm.locationLabel} -
- {location?.name}{' '} - -
-
+ <> + + {mapForm.locationLabel} +
+ {location?.name}{' '} + +
+
+ + ) } @@ -148,33 +153,40 @@ 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 [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 { addPinTitle, yourPinTitle } = headings.map + if (!user) { + return null + } - const formId = 'MapSection' const isMember = user?.profileType === ProfileTypeList.MEMBER + const { addPinTitle, yourPinTitle } = headings.map + const formId = 'MapSection' useEffect(() => { const init = async () => { if (!user) return - const pin = (await mapsStore.getPin(user.userName)) || null + const pin = await mapsStore.getPin(user.userName) + setMapPin(pin) + pin && setMarkerIcon(createMarkerIcon(pin, true)) setIsLoading(false) } init() }, [user, notification]) - if (!user) return null - const defaultLocation = { latlng: { lat: randomIntFromInterval(-90, 90), @@ -192,7 +204,8 @@ export const SettingsPageMapPin = () => { } const updatedUser = await userStore.updateUserLocation(updatingUser) if (updatedUser) { - await mapsStore.setUserPin(toJS(updatedUser)) + const pin = toJS(updatedUser) + await mapsStore.setUserPin(pin) } setNotification({ message: mapForm.succesfulSave, @@ -278,7 +291,7 @@ export const SettingsPageMapPin = () => { submitFailed={submitFailed} /> - + { return ( { + updatePosition={(newPosition: ILatLng) => { onChange({ latlng: newPosition }) }} + markerIcon={markerIcon} + zoom={2} + center={[0, 0]} /> ) }} /> - - - {fields.mapPinDescription.title} - - - -