Skip to content

Commit

Permalink
feat(maps): switch to self-hosted tiles (#1105)
Browse files Browse the repository at this point in the history
* refactor(map): migrate to our self-hosted map tiles
* feat(maps): use our own self-hosted tiles
* refactor(map): improve map popup and drawer
  • Loading branch information
vnugent authored Feb 27, 2024
1 parent 0f22935 commit c271962
Show file tree
Hide file tree
Showing 15 changed files with 361 additions and 203 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@
"immer": "^10.0.2",
"lexical": "^0.7.5",
"mapbox-gl": "^2.7.0",
"maplibre-gl": "^4.0.2",
"nanoid": "^4.0.0",
"nanoid-dictionary": "^4.3.0",
"next": "^13.5.6",
"next-auth": "^4.22.1",
"nprogress": "^0.2.0",
"paper": "^0.12.17",
"paperjs-offset": "^1.0.8",
"pmtiles": "^3.0.3",
"rc-slider": "^10.0.0-alpha.5",
"react": "^18.2.0",
"react-content-loader": "^6.2.0",
Expand All @@ -61,7 +63,7 @@
"react-hook-form": "^7.34.2",
"react-hotkeys-hook": "^3.4.7",
"react-infinite-scroll-component": "^6.1.0",
"react-map-gl": "^7.0.10",
"react-map-gl": "^7.1.7",
"react-markdown": "^9.0.1",
"react-paginate": "^8.1.3",
"react-responsive": "^9.0.0-beta.6",
Expand Down
1 change: 0 additions & 1 deletion src/app/(default)/area/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Link from 'next/link'
import { Metadata } from 'next'
import { validate } from 'uuid'
import { MapPinLine, Lightbulb, ArrowRight } from '@phosphor-icons/react/dist/ssr'
import 'mapbox-gl/dist/mapbox-gl.css'
import Markdown from 'react-markdown'

import PhotoMontage, { UploadPhotoCTA } from '@/components/media/PhotoMontage'
Expand Down
2 changes: 1 addition & 1 deletion src/app/(default)/components/ui/AreaPageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const AreaPageContainer: React.FC<{
{breadcrumbs == null ? <BreadCrumbsSkeleton /> : breadcrumbs}
{children == null ? <ContentSkeleton /> : children}
</div>
<div id='#map' className='w-full mt-16 relative h-[90vh] border-t snap-start snap-normal'>
<div id='map' className='w-full mt-16 relative h-[90vh] border-t snap-start snap-normal'>
{map != null && map}
</div>
</article>
Expand Down
34 changes: 34 additions & 0 deletions src/app/(maps)/components/FullScreenMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client'
import { useEffect, useState } from 'react'
import { GlobalMap } from '@/components/maps/GlobalMap'

export const FullScreenMap: React.FC = () => {
const [initialCenter, setInitialCenter] = useState<[number, number] | undefined>(undefined)

useEffect(() => {
getVisitorLocation().then((visitorLocation) => {
if (visitorLocation != null) {
setInitialCenter([visitorLocation.longitude, visitorLocation.latitude])
}
}).catch(() => {
console.log('Unable to determine user\'s location')
})
}, [])

return (
<GlobalMap
showFullscreenControl={false}
initialCenter={initialCenter}
/>
)
}

const getVisitorLocation = async (): Promise<{ longitude: number, latitude: number } | undefined> => {
try {
const res = await fetch('/api/geo')
return await res.json()
} catch (err) {
console.log('ERROR', err)
return undefined
}
}
2 changes: 1 addition & 1 deletion src/app/(maps)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'mapbox-gl/dist/mapbox-gl.css'
import 'maplibre-gl/dist/maplibre-gl.css'
import { Metadata } from 'next'
import '@/public/fonts/fonts.css'
import './../global.css'
Expand Down
6 changes: 2 additions & 4 deletions src/app/(maps)/maps/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { GlobalMap } from '@/components/maps/GlobalMap'
import { ProfileMenu } from '../components/ProfileMenu'
import { FullScreenMap } from '../components/FullScreenMap'

export const dynamic = 'force-dynamic'

export default async function MapPage (): Promise<any> {
return (
<div className='w-full h-full'>
<ProfileMenu />
<GlobalMap
showFullscreenControl={false}
/>
<FullScreenMap />
</div>
)
}
5 changes: 4 additions & 1 deletion src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,12 @@ A slightly deemphasized dotted underline for a tag in order to not competing wit
}

/**
* Force mapbox-gl library to use our font otherwise components inside the map
* Force mapbox-gl/maplibre library to use our font otherwise components inside the map
* will use their font and look out of place.
*/
.mapboxgl-map {
font-family: inherit !important;
}
.maplibregl-map {
font-family: inherit !important;
}
81 changes: 47 additions & 34 deletions src/components/maps/AreaInfoDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,72 @@
import * as Popover from '@radix-ui/react-popover'

import { MapAreaFeatureProperties } from './AreaMap'
import { MapAreaFeatureProperties, SimpleClimbType } from './GlobalMap'
import { getAreaPageFriendlyUrl } from '@/js/utils'
import { Card } from '../core/Card'
import { EntityIcon } from '@/app/(default)/editArea/[slug]/general/components/AreaItem'
import { ArrowRight } from '@phosphor-icons/react/dist/ssr'

/**
* Area info panel
*/
export const AreaInfoDrawer: React.FC<{ data: MapAreaFeatureProperties | null, onClose?: () => void }> = ({ data, onClose }) => {
const parent = data?.parent == null ? null : JSON.parse(data.parent)
const parentName = parent?.name ?? ''
const parentId = parent?.id ?? null
return (
<Popover.Root open={data != null}>
<Popover.Anchor className='absolute top-3 left-3 z-50' />
<Popover.Content align='start'>
{data != null && <Content {...data} parentName={parentName} parentId={parentId} />}
{data != null && <Content {...data} />}
</Popover.Content>
</Popover.Root>
)
}

export const Content: React.FC<MapAreaFeatureProperties & { parentName: string, parentId: string | null }> = ({ id, name, parentName, parentId, content }) => {
const url = parentId == null
? (
<div className='inline-flex items-center gap-1.5'>
<EntityIcon type='area' size={16} />
<span className='text-secondary font-medium'>{parentName}</span>
</div>
)
: (
<a
href={getAreaPageFriendlyUrl(parentId, name)}
className='inline-flex items-center gap-1.5'
>
<EntityIcon type='area' size={16} /><span className='text-secondary font-medium hover:underline '>{parentName}</span>
</a>
)

const friendlyUrl = getAreaPageFriendlyUrl(id, name)
export const Content: React.FC<MapAreaFeatureProperties> = ({ id, areaName, climbs, content: { description } }) => {
const friendlyUrl = getAreaPageFriendlyUrl(id, areaName)
const editUrl = `/editArea/${id}/general`
return (
<Card>
<div className='flex flex-col gap-y-1 text-xs'>
<div>{url}</div>
<div className='ml-2'>
<span className='text-secondary'>&#8735;</span><a href={getAreaPageFriendlyUrl(id, name)} className='text-sm font-medium hover:underline'>{name}</a>
</div>
</div>
<hr className='mt-6' />
<div className='flex items-center justify-end gap-2'>
<a className='btn btn-link btn-sm no-underline' href={`/editArea/${id}`}>Edit</a>
<a className='btn btn-primary btn-sm' href={friendlyUrl}>Visit area <ArrowRight /></a>
<div className='flex flex-col gap-4'>
<section className='flex flex-col gap-y-2'>
<div className='text-lg font-medium leading-snug tracking-tight'>{areaName}</div>
<div className='font-sm text-secondary flex items-center gap-1'>
<EntityIcon type='crag' size={16} />
·
<span className='text-xs font-medium'>{climbs.length} climbs</span>
<a href={friendlyUrl} className='text-info text-xs font-semibold ml-auto hover:underline'>Visit page</a>
</div>
</section>

<a className='btn btn-primary btn-outline btn-sm' href={editUrl}>Edit area</a>

<hr />

<section className='text-xs'>
{description == null || description.trim() === ''
? <p>No description available. <a className='text-info hover:underline' href={editUrl}>[Add]</a></p>
: <p>{description}</p>}
</section>

<hr />

<MicroClimbList climbs={climbs} />
</div>
</Card>
)
}

const MicroClimbList: React.FC<{ climbs: SimpleClimbType[] }> = ({ climbs }) => {
return (
<section>
<h3 className='text-base font-semibold text-secondary'>Climbs</h3>
<ol>
{climbs.map((climb) => {
const url = `/climb/${climb.id}`
return (
<li key={climb.id} className='text-xs'>
<a href={url} className='hover:underline'>{climb.name}</a>
</li>
)
})}
</ol>
</section>
)
}
33 changes: 11 additions & 22 deletions src/components/maps/AreaInfoHover.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,40 @@
import * as Popover from '@radix-ui/react-popover'
import { HoverInfo, MapAreaFeatureProperties } from './AreaMap'
import { getAreaPageFriendlyUrl } from '@/js/utils'
import { Card } from '../core/Card'
import { EntityIcon } from '@/app/(default)/editArea/[slug]/general/components/AreaItem'
import { SelectedPolygon } from './AreaActiveMarker'
import { HoverInfo, MapAreaFeatureProperties } from './GlobalMap'

/**
* Area info panel
*/
export const AreaInfoHover: React.FC<HoverInfo> = ({ data, geometry, mapInstance }) => {
const parent = data?.parent == null ? null : JSON.parse(data.parent)
const parentName = parent?.name ?? 'Unknown'
const parentId = parent?.id ?? null

let screenXY
if (geometry.type === 'Point') {
screenXY = mapInstance.project(geometry.coordinates)
} else {
return <SelectedPolygon geometry={geometry} />
}

return (
<Popover.Root defaultOpen>
<Popover.Anchor style={{ position: 'absolute', left: screenXY.x, top: screenXY.y }} />
<Popover.Content align='center' side='top' alignOffset={12}>
{data != null && <Content {...data} parentName={parentName} parentId={parentId} />}
<Popover.Arrow />
<Popover.Content align='center' side='top' sideOffset={8} collisionPadding={24} className='z-50'>
{data != null && <Content {...data} />}
</Popover.Content>
</Popover.Root>
)
}

export const Content: React.FC<MapAreaFeatureProperties & { parentName: string, parentId: string | null }> = ({ id, name, parentName, parentId }) => {
const url = parentId == null
? parentName
: (
<a
href={getAreaPageFriendlyUrl(parentId, name)}
className='inline-flex items-center gap-1.5'
>
<EntityIcon type='area' size={16} /><span className='text-secondary font-medium hover:underline '>{parentName}</span>
</a>
)
export const Content: React.FC<MapAreaFeatureProperties> = ({ id, areaName, climbs }) => {
return (
<Card>
<div className='flex flex-col gap-y-1 text-xs'>
<div>{url}</div>
<div className='ml-2'>
<span className='text-secondary'>&#8735;</span><a href={getAreaPageFriendlyUrl(id, name)} className='text-sm font-medium hover:underline'>{name}</a>
<a href={getAreaPageFriendlyUrl(id, areaName)} className='text-base font-medium hover:underline'>{areaName}</a>
<div className='font-sm text-secondary flex items-center gap-1'>
<EntityIcon type='crag' size={16} />
·
<span className='text-xs'>{climbs.length} climbs</span>
</div>
</div>
</Card>
Expand Down
Loading

0 comments on commit c271962

Please sign in to comment.