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

Create API for map pins search #3955

Merged
merged 10 commits into from
Oct 30, 2024
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"fuse.js": "^6.4.6",
"helmet": "^7.1.0",
"isbot": "^5.1.13",
"keyv": "^5.1.2",
"leaflet": "^1.5.1",
"leaflet.markercluster": "^1.4.1",
"mobx": "6.9.0",
Expand Down
19 changes: 10 additions & 9 deletions packages/cypress/src/integration/map.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const userId = 'davehakkens'
const userId = 'demo_user'
const profileTypesCount = 5
const urlLondon =
'https://nominatim.openstreetmap.org/search?format=json&q=london&accept-language=en'
Expand Down Expand Up @@ -29,7 +29,7 @@ describe('[Map]', () => {
cy.step('New map shows the cards')
cy.get('[data-cy="welome-header"]').should('be.visible')
cy.get('[data-cy="CardList-desktop"]').should('be.visible')
cy.get('[data-cy="list-results"]').contains('52 results in view')
cy.get('[data-cy="list-results"]').contains(/\d+ results in view/)

cy.step('Map filters can be used')
cy.get('[data-cy=MapFilterProfileTypeCardList]')
Expand All @@ -40,7 +40,14 @@ describe('[Map]', () => {
// Reduction in coverage until temp API removed
// cy.get('[data-cy="list-results"]').contains('6 results in view')
cy.get('[data-cy=MapListFilter-active]').first().click()
cy.get('[data-cy="list-results"]').contains('52 results in view')
cy.get('[data-cy="list-results"]').contains(/\d+ results in view/)

cy.step('Clusters show up')
cy.get('.icon-cluster-many')
.first()
.within(() => {
cy.get('.icon-cluster-text').contains(/\d+/)
})

cy.step('Users can select filters')
cy.get('[data-cy=MapFilterList]').should('not.exist')
Expand Down Expand Up @@ -116,11 +123,5 @@ describe('[Map]', () => {
cy.intercept(urlLondon).as('londonSearch')
cy.wait('@londonSearch')
cy.contains('London, Greater London, England, United Kingdom').click()

cy.get('.icon-cluster-many')
.should('be.visible')
.within(() => {
cy.get('.icon-cluster-text').should('have.text', '3').and('be.visible')
})
})
})
20 changes: 2 additions & 18 deletions src/pages/Maps/Maps.client.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useContext, useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { observer } from 'mobx-react'
import { useCommonStores } from 'src/common/hooks/useCommonStores'
import { filterMapPinsByType } from 'src/stores/Maps/filter'
import { MAP_GROUPINGS } from 'src/stores/Maps/maps.groupings'
import { Box } from 'theme-ui'
Expand Down Expand Up @@ -32,14 +31,12 @@ const MapsPage = observer(() => {
const [showNewMap, setShowNewMap] = useState<boolean>(false)
const [zoom, setZoom] = useState<number>(INITIAL_ZOOM)

const { userStore } = useCommonStores().stores
const navigate = useNavigate()
const location = useLocation()
const mapPinService = useContext(MapPinServiceContext) as IMapPinService

const mapRef = useRef<Map>(null)
const newMapRef = useRef<Map>(null)
const user = userStore.activeUser

useEffect(() => {
if (!selectedPin) {
Expand All @@ -51,7 +48,7 @@ const MapsPage = observer(() => {
const fetchMapPins = async () => {
setNotification('Loading...')
try {
const pins = await mapPinService.getMapPins(user?._id)
const pins = await mapPinService.getMapPins()
setMapPins(pins)
setNotification('')
} catch (error) {
Expand All @@ -75,19 +72,6 @@ const MapsPage = observer(() => {
}
}, [location.hash])

useEffect(() => {
const userName = user?._id
if (!userName || mapPins.find(({ _id }) => _id === userName)) {
return
}

mapPinService.getMapPinSelf(userName).then((userMapPin) => {
if (userMapPin && !mapPins.find((pin) => pin._id === userMapPin._id)) {
setMapPins((existingPins) => [...existingPins, userMapPin])
}
})
}, [user])

const promptUserLocation = async () => {
try {
const position = await GetLocation()
Expand Down Expand Up @@ -127,7 +111,7 @@ const MapsPage = observer(() => {
// If only the mapPins where preloaded with the "detail" property, i think that this could fly away
const pin = await mapPinService.getMapPinByUserId(userId)
if (pin) {
logger.info(`Fetched map pin by user id`, { userId })
logger.info(`Fetched map pin by user id`, { userId, pin })
setCenter(pin.location)
setSelectedPin(pin)
} else {
Expand Down
1 change: 0 additions & 1 deletion src/pages/Maps/Maps.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ const Wrapper = (path = '/map') => {
shortDescription: 'description',
},
}),
getMapPinSelf: vi.fn().mockResolvedValue({}),
getMapPins: vi.fn().mockImplementation(() => {
return Promise.resolve([])
}),
Expand Down
21 changes: 2 additions & 19 deletions src/pages/Maps/map.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { faker } from '@faker-js/faker'
import { DB_ENDPOINTS } from 'src/models/dbEndpoints'
import { describe, expect, it, vi } from 'vitest'

import { mapPinService } from './map.service'
Expand Down Expand Up @@ -27,7 +25,7 @@ describe('map.service', () => {
it('fetches map pins', async () => {
// prepare
global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve([{ _id: '1' }]),
json: () => Promise.resolve({ mapPins: [{ _id: '1' }] }),
})

// act
Expand All @@ -53,7 +51,7 @@ describe('map.service', () => {
it('fetches map pin by user id', async () => {
// prepare
global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve({ _id: '1' }),
json: () => Promise.resolve({ mapPin: { _id: '1' } }),
})

// act
Expand All @@ -74,19 +72,4 @@ describe('map.service', () => {
expect(result).toBeNull()
})
})

describe('getMapPinSelf', () => {
it('fetches user map pin', async () => {
// prepare
const userId = faker.internet.userName()

// act
await mapPinService.getMapPinSelf(userId)

// assert
expect(mockCollection).toHaveBeenCalledWith(DB_ENDPOINTS.mappins)
expect(mockWhere).toHaveBeenCalledWith('_id', '==', userId)
expect(mockGetDocs).toHaveBeenCalled()
})
})
})
68 changes: 7 additions & 61 deletions src/pages/Maps/map.service.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
import { createContext } from 'react'
import { collection, getDocs, query, where } from 'firebase/firestore'
import { API_URL } from 'src/config/config'
import { logger } from 'src/logger'
import { DB_ENDPOINTS } from 'src/models/dbEndpoints'
import { cdnImageUrl } from 'src/utils/cdnImageUrl'
import { firestore } from 'src/utils/firebase'

import type { IMapPin } from 'oa-shared'

export interface IMapPinService {
getMapPins: (currentUserId?: string) => Promise<IMapPin[]>
getMapPins: () => Promise<IMapPin[]>
getMapPinByUserId: (userName: string) => Promise<IMapPin | null>
getMapPinSelf: (userId: string) => Promise<IMapPin | null>
}

const getMapPins = async (currentUserId?: string) => {
const getMapPins = async () => {
try {
const response = await fetch(API_URL + '/map-pins')
const mapPins = await response.json()
const response = await fetch('/api/mappins/')
const { mapPins } = await response.json()

const hasCurrentUserPin = !!mapPins.find(({ _id }) => _id === currentUserId)

if (currentUserId && !hasCurrentUserPin) {
const userMapPin = await getMapPinByUserId(currentUserId)
userMapPin && mapPins.push(userMapPin)
}

return _transformCreatorImagesToCND(mapPins)
return mapPins
} catch (error) {
logger.error('Failed to fetch map pins', { error })
return []
Expand All @@ -35,8 +22,8 @@ const getMapPins = async (currentUserId?: string) => {

const getMapPinByUserId = async (userName: string) => {
try {
const response = await fetch(API_URL + '/map-pins/' + userName)
const mapPin = await response.json()
const response = await fetch('/api/mappins/' + userName)
const { mapPin } = await response.json()

return mapPin
} catch (error) {
Expand All @@ -45,50 +32,9 @@ const getMapPinByUserId = async (userName: string) => {
}
}

const getMapPinSelf = async (userId: string) => {
const collectionRef = collection(firestore, DB_ENDPOINTS.mappins)
const userMapPinQuery = query(collectionRef, where('_id', '==', userId))
const queryResults = await getDocs(userMapPinQuery)

if (!queryResults?.docs) {
logger.error('Invalid or empty response from query', { userId })
return null
}

const [userMapPin] = queryResults.docs

if (!userMapPin) {
logger.error('No map pin found for user', { userId })
return null
}

return userMapPin.data() as IMapPin
}

const _transformCreatorImagesToCND = (pins: IMapPin[]) => {
return pins.map((pin) => {
if (!pin.creator) {
return pin
}
return {
...pin,
creator: {
...pin.creator,
...(pin.creator.coverImage
? { coverImage: cdnImageUrl(pin.creator.coverImage, { width: 500 }) }
: {}),
...(pin.creator.userImage
? { userImage: cdnImageUrl(pin.creator.userImage, { width: 300 }) }
: {}),
},
}
})
}

export const MapPinServiceContext = createContext<IMapPinService | null>(null)

export const mapPinService: IMapPinService = {
getMapPins,
getMapPinByUserId,
getMapPinSelf,
}
88 changes: 88 additions & 0 deletions src/routes/api.mappins.$id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { json } from '@remix-run/node'
import { collection, getDocs, query, where } from 'firebase/firestore'
import { DB_ENDPOINTS } from 'src/models/dbEndpoints'
import { firestore } from 'src/utils/firebase'

import type { IMapPin } from 'oa-shared'

// runs on the server
export const loader = async ({ params }) => {
const id = params.id
const collectionRef = collection(firestore, DB_ENDPOINTS.mappins)
const userMapPinQuery = query(collectionRef, where('_id', '==', id))

const queryResults = await getDocs(userMapPinQuery)

const mapPin: IMapPin | undefined = queryResults.docs[0]?.data() as IMapPin

if (mapPin) {
const formattedMapPin = formatMapPinForResponse(mapPin)
const userProfile = await getUserProfile(id)
const userData = userProfile || {}

let heroImageUrl = ''
if (userData.coverImages && userData.coverImages.length) {
heroImageUrl = userData.coverImages[0].downloadUrl
}

// TODO: Load avatar from user profile
mariojsnunes marked this conversation as resolved.
Show resolved Hide resolved
const avatar = ''
const detail = {
heroImageUrl,
profilePicUrl: avatar,
shortDescription: userData.mapPinDescription
? userData.mapPinDescription
: '',
name: userData.userName,
displayName: userData.displayName,
profileUrl: `/u/${userData.userName}`,
verifiedBadge: !!userData.badges?.verified,
country:
userData.location?.countryCode ||
userData.country?.toLowerCase() ||
null,
}

return json({
mapPin: {
...formattedMapPin,
detail,
},
})
} else {
return json({ message: 'Not Found' }, { status: 404 })
}
}

export const action = async () => {
// create / edit a map-pin
}

function formatMapPinForResponse(data) {
return {
...data,
_deleted: !!data._deleted,
type: data.type || data.pinType,
verified: !!data.verified,
}
}

// taken from platform-api
async function getUserProfile(userId) {
const usersCollection = collection(firestore, DB_ENDPOINTS.users)

const usersByAuthId = await getDocs(
query(usersCollection, where('_authId', '==', userId)),
)
if (usersByAuthId.docs.length === 1) {
return usersByAuthId.docs[0].data()
}

const usersById = await getDocs(
query(usersCollection, where('_id', '==', userId)),
)

if (usersById.docs.length === 1) {
return usersById.docs[0].data()
}
}
Loading
Loading